Publish curated Unraid-AppOps-Gateway source from ba66029223865607deddc234e283c11127324eb8

This commit is contained in:
Public Source Publisher committed 2026-10-07 01:56:22 +00:00
1 parent 820244dbb3
commit 8dd4b40252
52 files changed
+11814 -288

No files matched your search

+42 -17
View File
@@ -1555,7 +1555,11 @@ def _execute_controller_artifact_promote(
reason: str,
original_policy_decision: str,
) -> dict[str, Any]:
from .controller_artifact_promoter import ControllerArtifactPromoter
from .controller_artifact_promoter import (
ControllerArtifactPromoter,
helper_failure_diagnostics,
safe_helper_diagnostics,
)
action = "controller.artifact.promote"
target = "tower/deployment-controller"
@@ -1595,12 +1599,43 @@ def _execute_controller_artifact_promote(
success = False
message = f"{type(exc).__name__}: controller artifact promotion failed"
diagnostics = None
if status == "failed" and result.get("reason") == "controller_promoter_helper_failed":
diagnostics = safe_helper_diagnostics(result.get("diagnostics"))
if diagnostics is None:
diagnostics = helper_failure_diagnostics(result.get("exitCode"))
exit_code = diagnostics["exitCode"]
exit_code_summary = str(exit_code) if exit_code is not None else "unknown"
message = (
f"Controller promotion helper failed: {diagnostics['code']}; "
f"phase={diagnostics['phase']}; exitCode={exit_code_summary}."
)
completed = complete_job(
session,
job,
status="succeeded" if success else "failed",
result_summary=message,
)
after_state = {
"status": status,
"revision": result.get("revision"),
"controllerVersion": result.get("controllerVersion"),
"artifactSha256": result.get("artifactSha256"),
}
redacted_metadata = {
"executor": action,
"integration": "mcp" if source == "chatgpt-mcp" else "operator",
"originalPolicyDecision": original_policy_decision,
"callerRevisionSelectable": False,
"callerCommandSelectable": False,
"callerMountSelectable": False,
"helperTargetSelectable": False,
}
if diagnostics is not None:
after_state["diagnostics"] = diagnostics
redacted_metadata["diagnostics"] = diagnostics
audit = log_event(
session,
source="action",
@@ -1613,23 +1648,10 @@ def _execute_controller_artifact_promote(
reason=reason,
result="success" if success else "failed",
execution_plan={"steps": steps, "jobId": completed.id},
after_state={
"status": status,
"revision": result.get("revision"),
"controllerVersion": result.get("controllerVersion"),
"artifactSha256": result.get("artifactSha256"),
},
redacted_metadata={
"executor": action,
"integration": "mcp" if source == "chatgpt-mcp" else "operator",
"originalPolicyDecision": original_policy_decision,
"callerRevisionSelectable": False,
"callerCommandSelectable": False,
"callerMountSelectable": False,
"helperTargetSelectable": False,
},
after_state=after_state,
redacted_metadata=redacted_metadata,
)
return {
operation_result = {
"success": success,
"status": "success" if success else status,
"result": "success" if success else "failed",
@@ -1642,6 +1664,9 @@ def _execute_controller_artifact_promote(
"controllerVersion": result.get("controllerVersion"),
"artifactSha256": result.get("artifactSha256"),
}
if diagnostics is not None:
operation_result["diagnostics"] = diagnostics
return operation_result
def _execute_runner_fleet_upgrade_v2(
+4
View File
@@ -178,6 +178,7 @@ ACTIONS = [
'authentik.unknown_mutation',
'forge.oidc.provision',
'forge.secrets.prepare',
'forge.route.stage',
'tautulli.terminate_session',
'tautulli.delete_newsletter',
'tautulli.delete_notification_log',
@@ -390,6 +391,7 @@ ACTION_RISK = {
'authentik.unknown_mutation': 'critical',
'forge.oidc.provision': 'critical',
'forge.secrets.prepare': 'critical',
'forge.route.stage': 'critical',
'tautulli.terminate_session': 'high',
'tautulli.delete_newsletter': 'critical',
'tautulli.delete_notification_log': 'critical',
@@ -539,6 +541,8 @@ def action_default_target(action: str) -> str:
return 'itworx-forge'
if action == 'forge.secrets.prepare':
return 'itworx-forge'
if action == 'forge.route.stage':
return 'itworx-forge'
if action == 'controller.artifact.promote':
return 'tower/deployment-controller'
if action == 'controller.managed-storage.initialize':
@@ -43,24 +43,68 @@ def helper_container_spec(
raise ValueError("controller_promoter_bootstrap_binding_invalid")
command = f"""set -Eeuo pipefail
phase=shell_setup
work=''
diagnostic_emitted=0
emit_failure() {{
local exit_code="$1"
if (( diagnostic_emitted == 0 )); then
printf 'APPOPS_CONTROLLER_PROMOTION_DIAGNOSTIC code=helper_failed phase=%s exitCode=%s\\n' "$phase" "$exit_code" >&2
diagnostic_emitted=1
fi
}}
on_error() {{
local exit_code=$?
trap - ERR
emit_failure "$exit_code"
exit "$exit_code"
}}
cleanup() {{
local exit_code=$?
local cleanup_exit_code=0
trap - EXIT
phase=cleanup
if [[ -n "$work" ]]; then
if rm -rf -- "$work"; then
:
else
cleanup_exit_code=$?
emit_failure "$cleanup_exit_code"
if (( exit_code == 0 )); then exit_code=$cleanup_exit_code; fi
fi
fi
exit "$exit_code"
}}
trap on_error ERR
umask 077
revision='{revision}'
version='{CONTROLLER_VERSION}'
image_id='{image_id}'
repo='/gitea-repositories/jens/unraid-appops-gateway.git'
[[ \"$(stat -c '%a:%u:%g' /tmp)\" == '1777:0:0' ]]
work=\"$(mktemp -d -p /tmp itworx-controller-artifact-promoter-{revision[:12]}.XXXXXX)\"
phase=tmp_root
[[ \"$(trap - ERR; stat -c '%a:%u:%g' /tmp)\" == '1777:0:0' ]]
phase=workspace_create
work=\"$(trap - ERR; mktemp -d -p /tmp itworx-controller-artifact-promoter-{revision[:12]}.XXXXXX)\"
phase=workspace_validate
[[ -d \"$work\" && ! -L \"$work\" && \"$(stat -c '%u:%a' \"$work\")\" == '0:700' ]]
cleanup() {{ rm -rf -- \"$work\"; }}
trap cleanup EXIT
phase=tool_git
command -v git >/dev/null
phase=tool_docker
command -v docker >/dev/null
phase=tool_compose
docker compose version >/dev/null
phase=tool_python
command -v python3 >/dev/null
phase=tool_tar
command -v tar >/dev/null
phase=tool_curl
command -v curl >/dev/null
phase=source_commit
git --git-dir=\"$repo\" cat-file -e \"$revision^{{commit}}\"
tree=\"$(git --git-dir=\"$repo\" rev-parse \"$revision^{{tree}}\")\"
phase=source_tree
tree=\"$(trap - ERR; git --git-dir=\"$repo\" rev-parse \"$revision^{{tree}}\")\"
phase=source_archive
git --git-dir=\"$repo\" archive --format=tar \"$revision\" \\
ops/gitea-deploy-broker \\
scripts/prepare_controller_artifact.py \\
@@ -69,10 +113,15 @@ git --git-dir=\"$repo\" archive --format=tar \"$revision\" \\
scripts/vendor \\
scripts/run_repopulse_adoption.py \
scripts/upgrade_deployment_controller.sh > \"$work/controller.tar\"
phase=archive_permissions
chmod 0600 \"$work/controller.tar\"
archive_sha=\"$(sha256sum \"$work/controller.tar\" | cut -d ' ' -f 1)\"
phase=archive_digest
archive_sha=\"$(trap - ERR; sha256sum \"$work/controller.tar\" | cut -d ' ' -f 1)\"
phase=upgrade_script
git --git-dir=\"$repo\" show \"$revision:scripts/upgrade_deployment_controller.sh\" > \"$work/upgrade.sh\"
phase=upgrade_permissions
chmod 0700 \"$work/upgrade.sh\"
phase=upgrade
bash \"$work/upgrade.sh\" \\
--archive \"$work/controller.tar\" \\
--archive-sha256 \"$archive_sha\" \\
@@ -10,16 +10,17 @@ import httpx
from ..connectors.portainer import PortainerConnector
from ..core.config import settings
from ..core.redaction import redact_message
from .controller_artifact_helper_spec import (
_IMAGE_ID,
_REVISION,
CONTROLLER_VERSION,
HELPER_LABEL,
HELPER_NAME,
SOURCE_REPOSITORY as SOURCE_REPOSITORY,
_IMAGE_ID,
_REVISION,
helper_container_spec,
)
from .controller_artifact_helper_spec import (
SOURCE_REPOSITORY as SOURCE_REPOSITORY,
)
from .deployment_broker import DeploymentBrokerClient
GITEA_OWNER = "Jens"
@@ -28,6 +29,132 @@ REQUIRED_PUSH_CONTEXT = "Managed validation / full (push)"
CONTROLLER_CONTAINER = "gitea-deploy-control"
_HELPER_TIMEOUT_SECONDS = 240
_TIMESTAMP = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$")
_MAX_HELPER_LOG_CHARS = 4000
_MAX_HELPER_LOG_LINES = 200
_MAX_HELPER_LOG_WIRE_BYTES = _MAX_HELPER_LOG_CHARS + (8 * _MAX_HELPER_LOG_LINES)
_HELPER_DIAGNOSTIC_PREFIX = "APPOPS_CONTROLLER_PROMOTION_DIAGNOSTIC"
_HELPER_DIAGNOSTIC_CODES = frozenset({"helper_failed"})
_HELPER_DIAGNOSTIC_PHASES = frozenset(
{
"shell_setup",
"tmp_root",
"workspace_create",
"workspace_validate",
"tool_git",
"tool_docker",
"tool_compose",
"tool_python",
"tool_tar",
"tool_curl",
"source_commit",
"source_tree",
"source_archive",
"archive_permissions",
"archive_digest",
"upgrade_script",
"upgrade_permissions",
"upgrade",
"cleanup",
}
)
_HELPER_DIAGNOSTIC_LINE = re.compile(
rf"^{_HELPER_DIAGNOSTIC_PREFIX} code=([a-z_]+) phase=([a-z_]+) "
r"exitCode=(0|[1-9][0-9]{0,2})$"
)
def helper_failure_diagnostics(exit_code: Any) -> dict[str, Any]:
safe_exit_code = exit_code if type(exit_code) is int and 0 <= exit_code <= 255 else None
return {"code": "helper_failed", "phase": "unknown", "exitCode": safe_exit_code}
def safe_helper_diagnostics(value: Any) -> dict[str, Any] | None:
if not isinstance(value, dict) or set(value) != {"code", "phase", "exitCode"}:
return None
code = value.get("code")
phase = value.get("phase")
exit_code = value.get("exitCode")
if not isinstance(code, str) or code not in _HELPER_DIAGNOSTIC_CODES:
return None
if not isinstance(phase, str) or (
phase != "unknown" and phase not in _HELPER_DIAGNOSTIC_PHASES
):
return None
if exit_code is not None and (
type(exit_code) is not int or not 0 <= exit_code <= 255
):
return None
return {"code": code, "phase": phase, "exitCode": exit_code}
def parse_helper_diagnostics(log_text: Any, exit_code: Any) -> dict[str, Any]:
fallback = helper_failure_diagnostics(exit_code)
if (
fallback["exitCode"] is None
or not isinstance(log_text, str)
or len(log_text) > _MAX_HELPER_LOG_CHARS
):
return fallback
candidates = [
line
for line in log_text.splitlines()
if line.startswith(_HELPER_DIAGNOSTIC_PREFIX)
]
if len(candidates) != 1:
return fallback
match = _HELPER_DIAGNOSTIC_LINE.fullmatch(candidates[0])
if match is None:
return fallback
code, phase, marker_exit_code = match.groups()
parsed_exit_code = int(marker_exit_code)
if (
code not in _HELPER_DIAGNOSTIC_CODES
or phase not in _HELPER_DIAGNOSTIC_PHASES
or parsed_exit_code != fallback["exitCode"]
):
return fallback
return {"code": code, "phase": phase, "exitCode": parsed_exit_code}
def _decode_docker_log_stream(content: Any) -> str | None:
if (
not isinstance(content, bytes)
or not content
or len(content) > _MAX_HELPER_LOG_WIRE_BYTES
):
return None
offset = 0
frame_count = 0
payload_size = 0
payloads: list[bytes] = []
while offset < len(content):
if frame_count >= _MAX_HELPER_LOG_LINES or len(content) - offset < 8:
return None
header = content[offset : offset + 8]
stream_type = header[0]
if stream_type not in {1, 2} or header[1:4] != b"\x00\x00\x00":
return None
frame_size = int.from_bytes(header[4:8], "big")
if frame_size == 0 or frame_size > _MAX_HELPER_LOG_CHARS - payload_size:
return None
payload_start = offset + 8
payload_end = payload_start + frame_size
if payload_end > len(content):
return None
payloads.append(content[payload_start:payload_end])
payload_size += frame_size
frame_count += 1
offset = payload_end
try:
return b"".join(payloads).decode("utf-8")
except UnicodeDecodeError:
return None
class ControllerArtifactPromotionError(RuntimeError):
@@ -257,10 +384,29 @@ class ControllerArtifactPromoter:
}
exit_code = state.get("ExitCode")
logs = self.portainer.get_container_logs(helper_id, tail=200)
log_lines = logs.get("logs") if isinstance(logs, dict) and isinstance(logs.get("logs"), list) else []
tail = "\n".join(str(line) for line in log_lines[-40:])[-4000:]
if exit_code != 0:
if type(exit_code) is not int or exit_code != 0:
try:
logs_response = self.portainer._request(
"GET",
f"/api/endpoints/{endpoint}/docker/containers/{quote(helper_id, safe='')}/logs",
params={
"stdout": 1,
"stderr": 1,
"tail": _MAX_HELPER_LOG_LINES,
},
)
log_text = (
_decode_docker_log_stream(logs_response.content)
if isinstance(logs_response, httpx.Response)
and logs_response.status_code == 200
else None
)
except (httpx.HTTPError, RuntimeError, ValueError):
log_text = None
diagnostics = parse_helper_diagnostics(
log_text,
exit_code,
)
try:
self._remove_helper(helper_id)
except RuntimeError:
@@ -269,15 +415,14 @@ class ControllerArtifactPromoter:
"status": "failed",
"revision": revision,
"reason": "controller_promoter_helper_failed",
"exitCode": exit_code,
"logTail": redact_message(tail),
"diagnostics": diagnostics,
}
try:
self._remove_helper(helper_id)
except RuntimeError:
pass
return {"status": "promoted", "revision": revision, "logTail": redact_message(tail)}
return {"status": "promoted", "revision": revision}
def run(self) -> dict[str, Any]:
revision = self._validated_main_revision()
+28 -3
View File
@@ -214,9 +214,7 @@ class DeploymentBrokerClient:
}
for field in error_response_fields:
value = decoded.get(field)
if isinstance(value, str) and len(value) <= 256:
rejected[field] = value
elif field == "noMutation" and isinstance(value, bool):
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}
@@ -279,6 +277,33 @@ class DeploymentBrokerClient:
"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,
@@ -26,6 +26,7 @@ import fcntl
import json
import os
from pathlib import Path
import re
import stat
import subprocess
import sys
@@ -36,6 +37,8 @@ host_path = Path("/controller-hub-releases/controller-config/projects.json")
lock_path = Path("/mnt/cache/appdata/gitea-deploy-runners/deploy.lock")
repository, revision, branch, raw_project = sys.argv[1:5]
project = json.loads(raw_project)
requested_project = project
strengthening_deadline = time.monotonic() + 20
def _validation_compatible(repository, validation):
expected_validation = {
@@ -58,6 +61,52 @@ def _validation_compatible(repository, validation):
"contexts": ["CI / gates (push)", "Managed validation / full (push)"],
}
def _source_verified_strengthening():
# Only an exact protected-main descriptor can strengthen an existing target.
# The controller reader uses fixed credential files and private stdin.
if branch != "main" or time.monotonic() >= strengthening_deadline:
return False
try:
fresh = git_run("rev-parse", "refs/heads/main")
if fresh.returncode or fresh.stdout.strip() != revision:
return False
result = git_run("show", f"{revision}:.itworx/deployment-target.json")
if result.returncode or len(result.stdout.encode("utf-8")) > 16384:
return False
def unique(pairs):
value = {}
for key, item in pairs:
if key in value:
raise ValueError("duplicate descriptor field")
value[key] = item
return value
descriptor = json.loads(result.stdout, object_pairs_hook=unique)
allowed = {"apiVersion", "applicationId", "composeFile", "composeProject",
"healthTimeout", "primaryService", "verifyOciRevision"}
if (not isinstance(descriptor, dict) or set(descriptor) - allowed
or descriptor.get("apiVersion") != "itworx.deployment-target/v1"
or descriptor.get("verifyOciRevision") is not True
or descriptor.get("composeFile") != compose_file
or descriptor.get("composeProject", descriptor.get("applicationId")) != requested_project["compose_project"]
or type(descriptor.get("healthTimeout", 180)) is not int
or descriptor.get("healthTimeout", 180) != requested_project["health_timeout"]
or not isinstance(descriptor.get("applicationId"), str)
or not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,62}", descriptor["applicationId"])
or not isinstance(descriptor.get("primaryService", descriptor["applicationId"]), str)
or not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,62}", descriptor.get("primaryService", descriptor["applicationId"]))):
return False
sys.path.insert(0, "/opt/gitea-deploy")
from status_server import _registered_legacy_api_reader
protected = _registered_legacy_api_reader(repository)(
f"/repos/{repository}/branches/main", query={},
)
return (time.monotonic() < strengthening_deadline
and isinstance(protected, dict) and protected.get("protected") is True
and isinstance(protected.get("commit"), dict)
and protected["commit"].get("id") == revision)
except Exception:
return False
for directory in (path.parent, host_path.parent, host_path.parent.parent):
info = directory.lstat()
if not stat.S_ISDIR(info.st_mode) or info.st_uid != 0 or info.st_mode & 0o022:
@@ -89,8 +138,16 @@ if not bare_repo.is_dir():
raise SystemExit(24)
def git_run(*args):
# Trust only this validated controller-owned mount, preserving Git's normal
# checks elsewhere. Always read original objects rather than replacement refs.
for directory in (bare_repo.parent.parent, bare_repo.parent, bare_repo):
info = directory.lstat()
if (not stat.S_ISDIR(info.st_mode) or info.st_uid not in {0, 1000}
or info.st_mode & 0o022):
raise ValueError("unsafe mounted repository")
return subprocess.run(
["git", f"--git-dir={bare_repo}", *args],
["git", "--no-replace-objects", "-c", f"safe.directory={bare_repo}",
f"--git-dir={bare_repo}", *args],
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
@@ -168,12 +225,34 @@ with lock_handle as lock:
"gitea_repository_id",
"gitea_webhook_secret_file",
"validation",
"legacy_containers",
}
extra_keys = set(current) - set(project)
if not extra_keys.issubset(allowed_extra_keys):
raise SystemExit(23)
canonical_current = {key: current.get(key) for key in project}
if canonical_current != project:
immutable_keys = set(project) - {"source_dir", "exact_source", "verify_oci_revision", "health_timeout"}
if any(current.get(key) != project[key] for key in immutable_keys):
raise SystemExit(23)
# Preserve the registered source mode and fixed historical appdata path.
# Registration must never silently migrate an existing runtime's source.
source = current.get("source_dir")
known_sources = {project["source_dir"], f"/srv/example/appdata/{project['slug']}/source"}
exact_source = current.get("exact_source", False)
verification = current.get("verify_oci_revision", False)
health_timeout = current.get("health_timeout", 180)
if (not isinstance(source, str) or source not in known_sources or type(exact_source) is not bool
or type(verification) is not bool or type(project["verify_oci_revision"]) is not bool
or type(health_timeout) is not int or health_timeout != project["health_timeout"]
or verification and project["verify_oci_revision"] is not True):
raise SystemExit(23)
legacy_containers = current.get("legacy_containers")
if legacy_containers is not None and (
not isinstance(legacy_containers, list) or len(legacy_containers) > 32
or any(not isinstance(name, str) or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,127}", name)
for name in legacy_containers)
or len(set(legacy_containers)) != len(legacy_containers)
or set(legacy_containers).intersection(project["containers"])
):
raise SystemExit(23)
env_source = current.get("env_source")
if env_source is not None:
@@ -222,6 +301,15 @@ with lock_handle as lock:
if not _validation_compatible(repository, validation):
raise SystemExit(23)
strengthened = not verification and project["verify_oci_revision"] is True
if strengthened and not _source_verified_strengthening():
raise SystemExit(39)
reconciled = dict(current)
if strengthened:
reconciled["verify_oci_revision"] = True
changed = reconciled != current
project = reconciled
if changed:
try:
backup_root = Path("/mnt/cache/appdata/gitea-deploy-runners/state/registry-backups")
@@ -266,11 +354,17 @@ with lock_handle as lock:
raise SystemExit(33)
try:
if current is not None and strengthened and not _source_verified_strengthening():
temporary.unlink(missing_ok=True)
raise SystemExit(39)
latest = mutation_path.lstat()
latest_identity = (latest.st_dev, latest.st_ino, latest.st_mode, latest.st_uid,
latest.st_gid, latest.st_mtime_ns, latest.st_ctime_ns, latest.st_size)
if latest_identity != registry_identity or mutation_path.read_bytes() != original or not os.path.samefile(path, mutation_path):
raise SystemExit(34)
if current is not None and strengthened and time.monotonic() >= strengthening_deadline:
temporary.unlink(missing_ok=True)
raise SystemExit(39)
os.replace(temporary, mutation_path)
except Exception:
try:
@@ -429,6 +523,8 @@ class DeploymentTargetRegistry:
verify_oci_revision: bool = False,
) -> dict[str, Any]:
try:
if type(verify_oci_revision) is not bool:
raise ValueError("OCI revision verification must be a boolean")
(
repository,
revision,
@@ -561,6 +657,7 @@ class DeploymentTargetRegistry:
36: "deployment projects registry host alias is unavailable or does not match the controller registry",
37: "controller project branch does not match the requested default branch",
38: "controller project Compose contract is invalid",
39: "existing target verification strengthening requires its exact protected-main source descriptor",
}
if exit_code in preflight_reasons:
return {
+174 -34
View File
@@ -15,6 +15,7 @@ import secrets
import uuid
from typing import Any, Literal
from sqlalchemy.exc import IntegrityError
from sqlmodel import Session, select
from ..connectors.authentik import AuthentikConnector
@@ -46,6 +47,20 @@ _CONTROLLER_EVIDENCE = {"status", "targetId", "repository", "applicationId", "ap
_PROXY_EVIDENCE = {"success", "status", "hostId", "hostname"}
_OIDC_EVIDENCE = {"success", "status", "applicationId", "providerId", "clientSecretCreated"}
_APPDATA_ROOT_STATUSES = {"ready", "needs_initialization", "unavailable"}
_CAPABILITY_REASON = re.compile(r"^[a-z][a-z0-9_]{0,79}$")
_RUNTIME_SECURITY_FEATURES = ("exactTargetPolicy", "hmacV2", "replayProtection")
def _safe_capability_reason(value: object) -> str:
reason = str(value or "")
return reason if _CAPABILITY_REASON.fullmatch(reason) else ""
def _features_ready(features: object, names: tuple[str, ...]) -> bool:
return bool(
isinstance(features, dict)
and all(features.get(name) is True for name in names)
)
def _trusted_appdata_roots_readiness(
@@ -57,7 +72,7 @@ def _trusted_appdata_roots_readiness(
if isinstance(reported, list):
if len(reported) != len(roots):
return [{"root": root, "status": unavailable[root]} for root in roots]
statuses: dict[str, str] = {}
statuses: dict[str, dict[str, str]] = {}
for item in reported:
if not isinstance(item, dict):
return [{"root": root, "status": unavailable[root]} for root in roots]
@@ -71,10 +86,14 @@ def _trusted_appdata_roots_readiness(
or status not in _APPDATA_ROOT_STATUSES
):
return [{"root": expected, "status": unavailable[expected]} for expected in roots]
statuses[root] = status
value = {"root": root, "status": status}
reason = _safe_capability_reason(item.get("reason"))
if reason:
value["reason"] = reason
statuses[root] = value
if set(statuses) != set(roots):
return [{"root": root, "status": unavailable[root]} for root in roots]
return [{"root": root, "status": statuses[root]} for root in roots]
return [statuses[root] for root in roots]
# A legacy V1 controller reports only its configured root. It cannot attest
# independent managed-volume readiness, so only its exact legacy root can
@@ -224,11 +243,40 @@ class EnrollmentPlane:
controller_root_matches = bool(selected_root and selected_root.get("status") == "ready")
else:
controller_root_matches = False
broker_ready = bool(
controller_core_ready = bool(
bootstrap
and self.broker.is_configured()
and controller_capability.get("status") == "ready"
)
features = controller_capability.get("features")
runtime_security_ready = _features_ready(features, _RUNTIME_SECURITY_FEATURES)
apply_ready = bool(
controller_core_ready
and runtime_security_ready
and isinstance(features, dict)
and features.get("deploymentApply") is True
)
verify_ready = bool(
controller_core_ready
and runtime_security_ready
and isinstance(features, dict)
and features.get("deploymentVerify") is True
)
rollback_ready = bool(
controller_core_ready
and runtime_security_ready
and isinstance(features, dict)
and features.get("rollback") is True
)
runtime_deployment_ready = apply_ready and verify_ready
enrollment_features_ready = _features_ready(
features,
("enrollment", "targetRegistration", "runtimeSecretSealing"),
)
target_enrollment_ready = bool(
controller_core_ready
and controller_root_matches
and enrollment_features_ready
)
controller_managed_storage = controller_capability.get("managedStorage")
if not isinstance(controller_managed_storage, dict):
@@ -248,6 +296,41 @@ class EnrollmentPlane:
else:
managed_storage_status = "unavailable"
managed_storage_blocker = "managed_appdata_volume_unavailable"
controller_roots_by_path = {
str(item.get("root") or ""): item
for item in available_roots
if isinstance(item, dict)
}
selected_root_readiness = (
controller_roots_by_path.get(trust.trusted_appdata_root)
if trust is not None
else None
)
controller_enrollment = controller_capability.get("enrollmentReadiness")
if not isinstance(controller_enrollment, dict):
controller_enrollment = {}
root_reason = _safe_capability_reason(
selected_root_readiness.get("reason")
if isinstance(selected_root_readiness, dict)
else None
)
if not root_reason and trust is not None and trust.trusted_appdata_root == MANAGED_DOCKER_APPDATA_ROOT:
root_reason = _safe_capability_reason(controller_managed_storage.get("reason"))
enrollment_reason = (
root_reason
or _safe_capability_reason(controller_enrollment.get("reason"))
or (
"enrollment_trust_not_configured"
if trust is None
else "trusted_appdata_root_unavailable"
if not controller_root_matches
else "controller_enrollment_feature_unavailable"
if not enrollment_features_ready
else "deployment_executor_unavailable"
)
)
if target_enrollment_ready:
enrollment_reason = ""
npm_ready = self.npm.is_configured()
oidc_configured = bool(
trust
@@ -262,18 +345,44 @@ class EnrollmentPlane:
def item(ready: bool, code: str) -> dict[str, str]:
return {"status": "ready" if ready else "unavailable", "blockerCode": "" if ready else code}
if not controller_core_ready:
enrollment_blocker = "deployment_executor_unavailable"
elif not controller_root_matches:
enrollment_blocker = "trusted_appdata_root_unavailable"
else:
enrollment_blocker = "controller_enrollment_feature_unavailable"
registry_report = controller_capability.get("registry")
registry_projection: dict[str, Any] = {"status": "unavailable"}
if isinstance(registry_report, dict):
registry_status = registry_report.get("status")
if registry_status in {"ready", "unavailable"}:
registry_projection["status"] = registry_status
count = registry_report.get("registeredProjectCount")
if (
registry_status == "ready"
and type(count) is int
and 0 <= count <= 500
):
registry_projection["registeredProjectCount"] = count
controller_reason = (
""
if controller_core_ready
else _safe_capability_reason(controller_capability.get("reason"))
or "deployment_executor_unavailable"
)
return {
"apiVersion": "itworx.enrollment-capabilities/v1",
"trust": item(configured, "enrollment_trust_not_configured"),
"targetEnrollment": item(broker_ready, "deployment_executor_unavailable"),
"targetEnrollment": item(target_enrollment_ready, enrollment_blocker),
"secretBootstrap": item(_enrollment_secret_store_ready(storage), "secret_store_not_production_ready"),
"runtimeDeployment": item(broker_ready, "deployment_executor_unavailable"),
"runtimeDeployment": item(runtime_deployment_ready, "deployment_executor_unavailable"),
"reverseProxyProvisioning": item(npm_ready, "npm_credentials_missing"),
"tlsProvisioning": item(certificate_ready, "certificate_provisioning_unavailable"),
"oidcProvisioning": item(oidc_configured, "oidc_profile_unavailable"),
"policyProvisioning": item(configured, "policy_template_unavailable"),
"rollback": item(broker_ready, "deployment_executor_unavailable"),
"verification": item(broker_ready, "deployment_executor_unavailable"),
"rollback": item(rollback_ready, "deployment_executor_unavailable"),
"verification": item(verify_ready, "deployment_executor_unavailable"),
"availableTrustedAppdataRoots": available_roots,
"managedStorage": {
"status": managed_storage_status,
@@ -283,19 +392,23 @@ class EnrollmentPlane:
"backupAcknowledged": bool(
trust and trust.managed_appdata_backup_acknowledged
),
},
"controller": {
"status": "ready" if broker_ready else "unavailable",
"reason": (
""
if broker_ready
else str(controller_capability.get("reason") or (
"trusted_appdata_root_mismatch"
if controller_capability.get("status") == "ready" and not controller_root_matches
else "deployment_executor_unavailable"
))
**(
{"reason": _safe_capability_reason(controller_managed_storage.get("reason"))}
if trust is not None
and trust.trusted_appdata_root == MANAGED_DOCKER_APPDATA_ROOT
and _safe_capability_reason(controller_managed_storage.get("reason"))
else {}
),
},
"enrollmentReadiness": {
"status": "ready" if target_enrollment_ready else "unavailable",
"reason": enrollment_reason,
},
"registry": registry_projection,
"controller": {
"status": "ready" if controller_core_ready else "unavailable",
"reason": controller_reason,
},
"profiles": [profile_summary(name) for name in ("internal-webapp", "internal-webapp-oidc", "internal-service", "repopulse-authenticated")],
}
@@ -433,6 +546,18 @@ class EnrollmentPlane:
if by_repository and (not by_application or by_repository.id != by_application.id):
checks.append(_check("target_already_exists", "blocked", "Repository is already bound to a different managed application."))
policy_ids = (
f"enrollment-{plan.application_id}-deployment-apply",
f"enrollment-{plan.application_id}-deployment-rollback",
)
policy_collisions = [policy_id for policy_id in policy_ids if session.get(Policy, policy_id) is not None]
checks.append(_check(
"exact_policy_id_collision",
"blocked" if policy_collisions else "passed",
"An enrollment-generated exact policy identifier is already owned and cannot be adopted."
if policy_collisions else "Both enrollment-generated exact policy identifiers are available.",
))
containers = self.portainer.list_containers()
if containers.get("status") != "ok":
checks.append(_check("container_inventory_unavailable", "blocked", "Portainer inventory is unavailable for collision protection."))
@@ -676,17 +801,25 @@ class EnrollmentPlane:
"sourceRevision": plan.revision,
"createdAt": utc_now().isoformat(),
}
identifiers: list[str] = []
for action, suffix in (("deployment.apply", "deployment-apply"), ("deployment.rollback", "deployment-rollback")):
policy_id = f"enrollment-{plan.application_id}-{suffix}"
existing = session.get(Policy, policy_id)
if existing is not None:
# Enrollment never adopts or repurposes a pre-existing policy,
# even where its visible action/target happen to match. A
# replay is handled before this method; this branch is a
# collision, not a safe recovery path.
raise EnrollmentError("exact policy identifier is already owned and cannot be adopted")
existing = Policy(
policy_specs = (
(
"deployment.apply",
f"enrollment-{plan.application_id}-deployment-apply",
),
(
"deployment.rollback",
f"enrollment-{plan.application_id}-deployment-rollback",
),
)
identifiers = [policy_id for _action, policy_id in policy_specs]
# Check the complete pair before adding either row. The database flush
# below is one transaction so a concurrent uniqueness collision cannot
# leave the first insert committed by _fail().
if any(session.get(Policy, policy_id) is not None for policy_id in identifiers):
raise EnrollmentError("exact policy identifier is already owned and cannot be adopted")
for action, policy_id in policy_specs:
policy = Policy(
id=policy_id,
action=action,
target=plan.application_id,
@@ -695,10 +828,17 @@ class EnrollmentPlane:
risk="high",
reason="Enrollment-generated exact-target deployment policy.",
)
existing.provenance_json = provenance
session.add(existing)
identifiers.append(policy_id)
session.commit()
policy.provenance_json = provenance
session.add(policy)
try:
# Force uniqueness errors inside this boundary; on failure discard
# the uncommitted unit of work before _fail() persists its outcome.
session.flush()
except IntegrityError as exc:
session.rollback()
raise EnrollmentError("exact policy identifier is already owned and cannot be adopted") from exc
# Deliberately do not commit here: the caller commits these rows together
# with the enrollment/job ready state after policy installation succeeds.
return identifiers
def _compensate(
+835
View File
@@ -0,0 +1,835 @@
from __future__ import annotations
import hashlib
import json
import re
import time
from typing import Any
from uuid import uuid4
from sqlmodel import Session, select
from ..connectors.nginx_proxy_manager import (
NginxProxyManagerConnector,
_NpmForgeRoutePermissionDenied,
)
from ..connectors.portainer import PortainerConnector
from ..db.models import DeploymentJob, Policy, Secret
from ..schemas.forge_bootstrap_apply import ForgeBootstrapApplyV1
from ..schemas.forge_route_stage import ForgeRouteStageResultV1
from ..schemas.forge_runtime_readiness import ForgeRuntimeReadinessResultV1
from .audit_logger import log_event
from .forge_bootstrap_apply import (
ForgeBootstrapApplyService,
_classify_controller_result,
_sha256,
)
from .forge_oidc_provisioning import _forge_oidc_process_lock
from .policy_engine import evaluate_policy
from .secret_store import get_secret, secret_storage_status, set_secret
_ACTION = "forge.route.stage"
_TARGET = "itworx-forge"
_REPOSITORY = "jens/itworx-forge"
_JOB_ID = re.compile(r"^forge-install-[0-9a-f]{64}$")
_IDEMPOTENCY = re.compile(r"^[A-Za-z0-9._:-]{8,128}$")
_OWNERSHIP_ID = re.compile(r"^froute-[0-9a-f]{32}$")
_SHA256 = re.compile(r"^[0-9a-f]{64}$")
_DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$")
_DOMAIN_LABEL = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$")
_ROUTE_HOST = "forge.example.com"
_ROUTE_NETWORK = "itworx-mcp-hub-npm"
_ROUTE_UPSTREAM = "http://itworx-forge:8080"
_INTENT_VERSION = "itworx.forge-route-stage-intent/v1"
_INTENT_PHASES = {"pending", "outcome_unknown", "host_staged_disabled", "aborted_before_post"}
_READINESS_SERVICES = ("api", "dashboard", "itworx-forge", "postgres", "worker")
def _canonical_json(value: Any) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
def _reject_duplicate_json_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
value: dict[str, Any] = {}
for key, item in pairs:
if key in value:
raise ValueError("duplicate journal key")
value[key] = item
return value
def _reject_json_constant(_constant: str) -> Any:
raise ValueError("invalid journal number")
def _readiness_is_fresh(identity: dict[str, Any]) -> bool:
observed_at = identity.get("observedAt")
if type(observed_at) is not int:
return False
now = int(time.time())
return now - 30 <= observed_at <= now + 5
def _journal_secret_ref(job_id: str) -> str:
# set_secret normalizes reference names to uppercase before persistence.
return ("FORGE_ROUTE_STAGE_" + hashlib.sha256(job_id.encode("ascii")).hexdigest()).upper()
def _secret_store_ready() -> bool:
try:
status = secret_storage_status()
except Exception: # noqa: BLE001 - secret-store diagnostics never enter route results.
return False
return isinstance(status, dict) and status.get("productionReady") is True
def _exact_route_policy_allowed(session: Session) -> str | None:
rows = session.exec(select(Policy).where(Policy.action == _ACTION)).all()
if len(rows) != 1:
return None
policy = rows[0]
if (
policy.target != _TARGET
or policy.manual != "allowed"
or policy.voice != "blocked"
or policy.risk != "critical"
):
return None
decision = evaluate_policy(session, _ACTION, _TARGET, "manual")
if (
decision.get("decision") != "allowed"
or decision.get("scope_decision") != "allowed"
or decision.get("policy_id") != policy.id
):
return None
return policy.id
def _result(job_id: str, status: str, message: str) -> dict[str, Any]:
staged = status == "host_staged_disabled"
return ForgeRouteStageResultV1.model_validate(
{
"success": staged,
"status": status,
"message": message,
"target": _TARGET,
"jobId": job_id,
"hostname": _ROUTE_HOST,
"upstream": _ROUTE_UPSTREAM,
"routeState": "host_staged_disabled" if staged else "not_routed",
"routed": False,
}
).model_dump(mode="json", by_alias=True)
def _audit(
session: Session,
*,
actor_identity: str,
decision: str,
policy_id: str | None,
result: dict[str, Any],
idempotency_hash: str,
) -> bool:
try:
log_event(
session,
source="manual",
identity=actor_identity,
action_id=_ACTION,
target=_TARGET,
target_type="forge_route_stage",
risk="critical",
policy_decision=decision,
reason=result["message"],
result=result["status"],
after_state={
"status": result["status"],
"routeState": result["routeState"],
"routed": False,
},
redacted_metadata={
"policyId": policy_id,
"idempotencyKeySha256": idempotency_hash,
},
)
except Exception: # noqa: BLE001 - audit/DB failures never expose held connector data.
try:
session.rollback()
except Exception: # noqa: BLE001 - keep DB details out of the response.
pass
return False
return True
def _verified_job(session: Session, job_id: str) -> tuple[DeploymentJob, ForgeBootstrapApplyV1] | None:
if _JOB_ID.fullmatch(job_id) is None:
return None
try:
job = session.get(DeploymentJob, job_id)
if (
job is None
or job.application_id != _TARGET
or job.repository != _REPOSITORY
or job.status != "deployed"
or job.stage not in {"controller_deployed", "resume_controller_deployed"}
or job.completed_at is None
or not isinstance(job.broker_request_id, str)
or _SHA256.fullmatch(job.broker_request_id) is None
or not isinstance(job.plan_json, dict)
or _sha256(job.plan_json) != job.plan_sha256
):
return None
profile = ForgeBootstrapApplyV1.model_validate(job.plan_json)
preflight_receipt = job.preflight_json
if (
job.revision != profile.revision
or not isinstance(preflight_receipt, dict)
or set(preflight_receipt) != {"apiVersion", "jobId", "sha256"}
or preflight_receipt
!= {
"apiVersion": "itworx.forge-bootstrap-preflight/v1",
"jobId": profile.preflight_job_id,
"sha256": profile.preflight_sha256,
}
):
return None
evidence = job.evidence_json
if (
not isinstance(evidence, dict)
or set(evidence) != {"profile", "controller"}
or evidence.get("profile") != "forge-first-install"
):
return None
historical_status, _stage, _summary, _safe_evidence = _classify_controller_result(
evidence.get("controller"),
request_id=job.broker_request_id,
profile=profile,
)
if historical_status != "deployed":
return None
return job, profile
except Exception: # noqa: BLE001 - invalid stored job data is a fixed denial.
return None
def _readiness_identity(
value: object,
*,
job: DeploymentJob,
profile: ForgeBootstrapApplyV1,
) -> dict[str, Any] | None:
try:
readiness = ForgeRuntimeReadinessResultV1.model_validate(value)
except Exception: # noqa: BLE001 - the current readiness projection is untrusted input.
return None
now = int(time.time())
artifact = readiness.controller_artifact
if (
readiness.status != "ready"
or readiness.target != _TARGET
or readiness.job_id != job.id
or readiness.revision != profile.revision
or readiness.workspace_image_digest != profile.workspace_image_digest
or readiness.frontdoor_image_digest != profile.frontdoor_image_digest
or type(readiness.observed_at) is not int
or readiness.observed_at < now - 30
or readiness.observed_at > now + 5
or readiness.migration_completed is not True
or readiness.health.status != "healthy"
or tuple(item.name for item in readiness.health.services) != _READINESS_SERVICES
or any(item.status != "healthy" for item in readiness.health.services)
or artifact is None
or artifact.source_repository != "jens/unraid-appops-gateway"
or not re.fullmatch(r"[0-9a-f]{40}", artifact.source_revision)
or not re.fullmatch(r"[0-9a-f]{40}", artifact.source_tree)
or not _SHA256.fullmatch(artifact.artifact_sha256)
):
return None
identity = {
"jobId": job.id,
"observedAt": readiness.observed_at,
"revision": profile.revision,
"workspaceImageDigest": profile.workspace_image_digest,
"frontdoorImageDigest": profile.frontdoor_image_digest,
"migrationCompleted": True,
"healthServices": list(_READINESS_SERVICES),
"controllerArtifact": {
"sourceRepository": artifact.source_repository,
"sourceRevision": artifact.source_revision,
"sourceTree": artifact.source_tree,
"artifactSha256": artifact.artifact_sha256,
},
}
return {"sha256": hashlib.sha256(_canonical_json(identity)).hexdigest(), **identity}
def _network_ready(value: object) -> bool:
return bool(
isinstance(value, dict)
and set(value) == {"status", "network", "npmAttached", "frontdoorAttached"}
and value.get("status") == "ready"
and value.get("network") == _ROUTE_NETWORK
and value.get("npmAttached") is True
and value.get("frontdoorAttached") is True
)
def _forge_route_npm_session(npm: NginxProxyManagerConnector) -> Any | None:
"""Require the connector's fixed route-only HTTPS transport contract."""
factory = getattr(npm, "forge_route_session", None)
if not callable(factory):
return None
try:
route_session = factory()
except Exception: # noqa: BLE001 - connector configuration is private.
return None
endpoint_sha256 = getattr(route_session, "endpoint_sha256", None)
if (
getattr(route_session, "verified_tls", None) is not True
or not isinstance(endpoint_sha256, str)
or _SHA256.fullmatch(endpoint_sha256) is None
):
return None
return route_session
def _desired_state(ownership_id: str) -> dict[str, Any]:
return {
"domain_names": [_ROUTE_HOST],
"forward_scheme": "http",
"forward_host": "itworx-forge",
"forward_port": 8080,
"access_list_id": 0,
"certificate_id": 0,
"enabled": False,
"ssl_forced": False,
"http2_support": False,
"hsts_enabled": False,
"hsts_subdomains": False,
"caching_enabled": False,
"block_exploits": True,
"allow_websocket_upgrade": False,
"trust_forwarded_proto": False,
"locations": [],
"meta": {"appops_forge_route_id": ownership_id},
"advanced_config": "",
}
def _build_record(
*,
job: DeploymentJob,
profile: ForgeBootstrapApplyV1,
preflight_receipt: dict[str, Any],
idempotency_hash: str,
readiness: dict[str, Any],
ownership_id: str,
npm_endpoint_sha256: str,
) -> dict[str, Any]:
return {
"apiVersion": _INTENT_VERSION,
"jobId": job.id,
"idempotencyKeySha256": idempotency_hash,
"npmEndpointSha256": npm_endpoint_sha256,
"ownershipId": ownership_id,
"phase": "pending",
"target": _TARGET,
"repository": _REPOSITORY,
"revision": profile.revision,
"workspaceImageDigest": profile.workspace_image_digest,
"frontdoorImageDigest": profile.frontdoor_image_digest,
"preflightReceiptSha256": preflight_receipt["sha256"],
"readinessIdentity": readiness,
"desired": _desired_state(ownership_id),
}
def _record_valid(value: object, job_id: str) -> bool:
if not isinstance(value, dict) or set(value) != {
"apiVersion",
"jobId",
"idempotencyKeySha256",
"npmEndpointSha256",
"ownershipId",
"phase",
"target",
"repository",
"revision",
"workspaceImageDigest",
"frontdoorImageDigest",
"preflightReceiptSha256",
"readinessIdentity",
"desired",
}:
return False
owner = value.get("ownershipId")
identity = value.get("readinessIdentity")
if (
value.get("apiVersion") != _INTENT_VERSION
or value.get("jobId") != job_id
or not isinstance(value.get("idempotencyKeySha256"), str)
or _SHA256.fullmatch(value["idempotencyKeySha256"]) is None
or not isinstance(value.get("npmEndpointSha256"), str)
or _SHA256.fullmatch(value["npmEndpointSha256"]) is None
or not isinstance(owner, str)
or _OWNERSHIP_ID.fullmatch(owner) is None
or value.get("phase") not in _INTENT_PHASES
or value.get("target") != _TARGET
or value.get("repository") != _REPOSITORY
or not isinstance(value.get("revision"), str)
or re.fullmatch(r"[0-9a-f]{40}", value["revision"]) is None
or not isinstance(value.get("workspaceImageDigest"), str)
or _DIGEST.fullmatch(value["workspaceImageDigest"]) is None
or not isinstance(value.get("frontdoorImageDigest"), str)
or _DIGEST.fullmatch(value["frontdoorImageDigest"]) is None
or not isinstance(value.get("preflightReceiptSha256"), str)
or _SHA256.fullmatch(value["preflightReceiptSha256"]) is None
or not isinstance(identity, dict)
or set(identity)
!= {
"sha256",
"jobId",
"observedAt",
"revision",
"workspaceImageDigest",
"frontdoorImageDigest",
"migrationCompleted",
"healthServices",
"controllerArtifact",
}
or not isinstance(identity.get("sha256"), str)
or _SHA256.fullmatch(identity["sha256"]) is None
or identity.get("jobId") != job_id
or type(identity.get("observedAt")) is not int
or identity.get("revision") != value.get("revision")
or identity.get("workspaceImageDigest") != value.get("workspaceImageDigest")
or identity.get("frontdoorImageDigest") != value.get("frontdoorImageDigest")
or identity.get("migrationCompleted") is not True
or identity.get("healthServices") != list(_READINESS_SERVICES)
or not isinstance(identity.get("controllerArtifact"), dict)
or not isinstance(value.get("desired"), dict)
or value["desired"] != _desired_state(owner)
):
return False
if hashlib.sha256(
_canonical_json({key: item for key, item in identity.items() if key != "sha256"})
).hexdigest() != identity["sha256"]:
return False
artifact = identity["controllerArtifact"]
return bool(
set(artifact)
== {"sourceRepository", "sourceRevision", "sourceTree", "artifactSha256"}
and artifact.get("sourceRepository") == "jens/unraid-appops-gateway"
and isinstance(artifact.get("sourceRevision"), str)
and re.fullmatch(r"[0-9a-f]{40}", artifact["sourceRevision"]) is not None
and isinstance(artifact.get("sourceTree"), str)
and re.fullmatch(r"[0-9a-f]{40}", artifact["sourceTree"])
and isinstance(artifact.get("artifactSha256"), str)
and _SHA256.fullmatch(artifact["artifactSha256"]) is not None
)
def _load_record(session: Session, job_id: str) -> tuple[str, dict[str, Any] | None]:
secret_ref = _journal_secret_ref(job_id)
row = session.get(Secret, secret_ref)
if row is None:
return "missing", None
raw = get_secret(session, secret_ref, touch=False)
if not isinstance(raw, str) or len(raw) > 16_384:
return "invalid", None
try:
value = json.loads(
raw,
object_pairs_hook=_reject_duplicate_json_keys,
parse_constant=_reject_json_constant,
)
except (TypeError, ValueError):
return "invalid", None
if (
not isinstance(value, dict)
or _canonical_json(value).decode("utf-8") != raw
or not _record_valid(value, job_id)
):
return "invalid", None
return "present", value
def _save_record(session: Session, record: dict[str, Any], *, commit: bool = True) -> bool:
try:
if not _record_valid(record, record.get("jobId", "")):
return False
set_secret(
session,
_journal_secret_ref(record["jobId"]),
_canonical_json(record).decode("utf-8"),
used_by=[_ACTION],
metadata={"kind": "encrypted_forge_route_stage_intent"},
commit=commit,
)
return True
except Exception: # noqa: BLE001 - secret-store/database errors remain fixed failures.
try:
session.rollback()
except Exception: # noqa: BLE001
pass
return False
def _normalized_domain(value: object) -> str | None:
if (
not isinstance(value, str)
or not value
or len(value) > 254
or any(char.isspace() or ord(char) < 32 or ord(char) == 127 for char in value)
):
return None
domain = value[:-1] if value.endswith(".") else value
if not domain or domain.endswith("."):
return None
wildcard = domain.startswith("*.")
if "*" in domain and not wildcard:
return None
suffix = domain[2:] if wildcard else domain
try:
ascii_domain = suffix.encode("idna").decode("ascii").lower()
except UnicodeError:
return None
labels = ascii_domain.split(".")
if (
not ascii_domain
or len(ascii_domain) > 253
or any(not label or len(label) > 63 or _DOMAIN_LABEL.fullmatch(label) is None for label in labels)
):
return None
return f"*.{ascii_domain}" if wildcard else ascii_domain
def _domain_matches(value: str) -> bool:
normalized = _normalized_domain(value)
if normalized == _ROUTE_HOST:
return True
if normalized is not None and normalized.startswith("*."):
suffix = normalized[2:]
# Treat any ancestor wildcard as a possible NPM match, including broad
# patterns such as *.tech, so route creation fails closed on overlap.
return _ROUTE_HOST.endswith(f".{suffix}")
return False
def _inventory_matches(value: object) -> tuple[str, list[dict[str, Any]]]:
if not isinstance(value, list):
return "unknown", []
matches: list[dict[str, Any]] = []
for host in value:
if not isinstance(host, dict):
return "unknown", []
domains = host.get("domain_names")
if not isinstance(domains, list) or any(_normalized_domain(domain) is None for domain in domains):
return "unknown", []
if any(_domain_matches(domain) for domain in domains):
matches.append(host)
return "ok", matches
def _host_is_exact_disabled(host: dict[str, Any], record: dict[str, Any]) -> bool:
expected = record["desired"]
for field in (
"forward_scheme",
"forward_host",
"advanced_config",
):
if type(host.get(field)) is not str or host[field] != expected[field]:
return False
for field in (
"forward_port",
"access_list_id",
"certificate_id",
):
if type(host.get(field)) is not int or host[field] != expected[field]:
return False
for field in (
"enabled",
"ssl_forced",
"http2_support",
"hsts_enabled",
"hsts_subdomains",
"caching_enabled",
"block_exploits",
"allow_websocket_upgrade",
"trust_forwarded_proto",
):
if type(host.get(field)) is not bool or host[field] is not expected[field]:
return False
host_id = host.get("id")
if type(host_id) is not int or host_id < 1:
return False
meta = host.get("meta")
return bool(
host.get("domain_names") == [_ROUTE_HOST]
and isinstance(meta, dict)
and meta.get("appops_forge_route_id") == record["ownershipId"]
and type(host.get("locations")) is list
and host["locations"] == []
)
def _inspect_inventory(value: object, record: dict[str, Any] | None) -> str:
status, matches = _inventory_matches(value)
if status != "ok":
return "unknown"
if len(matches) > 1:
return "conflict"
if not matches:
return "missing"
if record is None:
return "conflict"
host = matches[0]
meta = host.get("meta")
if not isinstance(meta, dict) or meta.get("appops_forge_route_id") != record["ownershipId"]:
return "conflict"
return "staged" if _host_is_exact_disabled(host, record) else "conflict"
def _reconcile_locked(
session: Session,
*,
job_id: str,
record: dict[str, Any],
npm: NginxProxyManagerConnector,
defer_phase_write: bool = False,
) -> dict[str, Any]:
endpoint_sha256 = getattr(npm, "endpoint_sha256", None)
if (
getattr(npm, "verified_tls", None) is not True
or not isinstance(endpoint_sha256, str)
or _SHA256.fullmatch(endpoint_sha256) is None
):
return _result(job_id, "outcome_unknown", "forge_route_reconcile_unavailable")
if endpoint_sha256 != record.get("npmEndpointSha256"):
return _result(job_id, "conflict", "forge_route_endpoint_binding_changed")
try:
hosts = npm.list_forge_route_hosts()
except Exception: # noqa: BLE001 - do not expose NPM responses, URLs or credentials.
return _result(job_id, "outcome_unknown", "forge_route_reconcile_unavailable")
state = _inspect_inventory(hosts, record)
if state == "staged":
if record.get("phase") != "host_staged_disabled":
record["phase"] = "host_staged_disabled"
if not defer_phase_write and not _save_record(session, record):
return _result(job_id, "outcome_unknown", "forge_route_journal_update_failed")
return _result(job_id, "host_staged_disabled", "forge_route_host_staged_disabled_not_routed")
if state == "conflict":
return _result(job_id, "conflict", "forge_route_ownership_or_state_conflict")
if state == "missing" and record.get("phase") == "aborted_before_post":
return _result(job_id, "blocked", "forge_route_policy_changed_before_mutation")
return _result(job_id, "outcome_unknown", "forge_route_host_state_unconfirmed")
def stage_forge_route(
session: Session,
*,
job_id: str,
idempotency_key: str,
actor_identity: str,
npm: NginxProxyManagerConnector | None = None,
portainer: PortainerConnector | None = None,
) -> dict[str, Any]:
"""Stage one disabled, fixed Forge host after current readiness is proven."""
normalized_key = idempotency_key.strip() if isinstance(idempotency_key, str) else ""
idempotency_hash = hashlib.sha256(normalized_key.encode("utf-8")).hexdigest()
actor = actor_identity.strip() if isinstance(actor_identity, str) else ""
connector = npm or NginxProxyManagerConnector()
network_reader = portainer or PortainerConnector()
policy_id: str | None = None
decision = "blocked"
mutation_attempted = False
outcome = _result(job_id, "blocked", "forge_route_stage_requirements_not_met")
try:
if _JOB_ID.fullmatch(job_id) is None or _IDEMPOTENCY.fullmatch(normalized_key) is None or not actor:
outcome = _result(job_id, "blocked", "forge_route_request_invalid")
else:
with _forge_oidc_process_lock():
policy_id = _exact_route_policy_allowed(session)
if policy_id is None:
outcome = _result(job_id, "blocked", "forge_route_exact_manual_policy_required")
else:
decision = "allowed"
record_state, record = _load_record(session, job_id)
if record_state == "invalid":
outcome = _result(job_id, "conflict", "forge_route_journal_invalid")
elif record_state == "present" and record is not None:
if record["idempotencyKeySha256"] != idempotency_hash:
outcome = _result(job_id, "conflict", "forge_route_job_already_claimed")
else:
route_npm = _forge_route_npm_session(connector)
if route_npm is None:
outcome = _result(job_id, "outcome_unknown", "forge_route_reconcile_unavailable")
else:
outcome = _reconcile_locked(
session, job_id=job_id, record=record, npm=route_npm
)
else:
verified = _verified_job(session, job_id)
if verified is None:
outcome = _result(job_id, "blocked", "forge_route_successful_job_required")
elif not _secret_store_ready():
outcome = _result(job_id, "blocked", "forge_route_encrypted_journal_required")
else:
job, profile = verified
route_npm = _forge_route_npm_session(connector)
if route_npm is None:
outcome = _result(job_id, "blocked", "forge_route_verified_npm_tls_required")
else:
try:
initial_hosts = route_npm.list_forge_route_hosts()
except _NpmForgeRoutePermissionDenied:
outcome = _result(
job_id,
"blocked",
"forge_route_npm_permission_required",
)
except Exception: # noqa: BLE001 - no connector diagnostics escape.
outcome = _result(job_id, "failed", "forge_route_npm_read_unavailable")
else:
inventory_state, matches = _inventory_matches(initial_hosts)
if inventory_state != "ok":
outcome = _result(job_id, "failed", "forge_route_npm_inventory_invalid")
elif matches:
outcome = _result(job_id, "conflict", "forge_route_existing_host_conflict")
elif not _network_ready(network_reader.forge_route_network_readiness()):
outcome = _result(job_id, "blocked", "forge_route_shared_network_not_ready")
else:
readiness_value = ForgeBootstrapApplyService().current_readiness(
session, job_id
)
readiness = _readiness_identity(
readiness_value, job=job, profile=profile
)
if readiness is None:
outcome = _result(job_id, "blocked", "forge_route_current_readiness_required")
else:
# Last authorization check occurs in the existing Forge lock,
# after fresh readiness and immediately before the journal/POST.
if _exact_route_policy_allowed(session) != policy_id:
outcome = _result(job_id, "blocked", "forge_route_exact_manual_policy_required")
else:
ownership_id = "froute-" + uuid4().hex
record = _build_record(
job=job,
profile=profile,
preflight_receipt=job.preflight_json,
idempotency_hash=idempotency_hash,
readiness=readiness,
ownership_id=ownership_id,
npm_endpoint_sha256=route_npm.endpoint_sha256,
)
if not _save_record(session, record):
outcome = _result(job_id, "failed", "forge_route_journal_persist_failed")
elif _exact_route_policy_allowed(session) != policy_id:
record["phase"] = "aborted_before_post"
_save_record(session, record)
outcome = _result(job_id, "blocked", "forge_route_exact_manual_policy_required")
elif not _readiness_is_fresh(record["readinessIdentity"]):
record["phase"] = "aborted_before_post"
_save_record(session, record)
outcome = _result(job_id, "blocked", "forge_route_current_readiness_expired")
else:
mutation_attempted = True
try:
route_npm.create_forge_disabled_proxy_host(ownership_id)
except _NpmForgeRoutePermissionDenied:
mutation_attempted = False
record["phase"] = "aborted_before_post"
if _save_record(session, record):
outcome = _result(
job_id,
"blocked",
"forge_route_npm_permission_required",
)
else:
outcome = _result(
job_id,
"outcome_unknown",
"forge_route_journal_update_failed",
)
except Exception: # noqa: BLE001 - a failed POST is ambiguous.
record["phase"] = "outcome_unknown"
_save_record(session, record)
if mutation_attempted:
outcome = _reconcile_locked(
session,
job_id=job_id,
record=record,
npm=route_npm,
)
except Exception: # noqa: BLE001 - no lock, connector, or DB detail reaches the caller.
outcome = _result(
job_id,
"outcome_unknown" if mutation_attempted else "failed",
"forge_route_operation_unavailable",
)
if not _audit(
session,
actor_identity=actor or "manual",
decision=decision,
policy_id=policy_id,
result=outcome,
idempotency_hash=idempotency_hash,
):
return _result(
job_id,
"outcome_unknown" if mutation_attempted else "failed",
"forge_route_audit_persist_failed",
)
return outcome
def reconcile_forge_route(
session: Session,
*,
job_id: str,
actor_identity: str,
npm: NginxProxyManagerConnector | None = None,
) -> dict[str, Any]:
"""Read back the fixed host for an existing operation; this function never POSTs."""
connector = npm or NginxProxyManagerConnector()
actor = actor_identity.strip() if isinstance(actor_identity, str) else ""
if _JOB_ID.fullmatch(job_id) is None:
return _result(job_id, "not_found", "forge_route_operation_not_found")
if not actor:
return _result(job_id, "failed", "forge_route_reconcile_unavailable")
try:
with _forge_oidc_process_lock():
state, record = _load_record(session, job_id)
if state == "missing":
return _result(job_id, "not_found", "forge_route_operation_not_found")
if state != "present" or record is None:
return _result(job_id, "outcome_unknown", "forge_route_journal_invalid")
route_npm = _forge_route_npm_session(connector)
if route_npm is None:
return _result(job_id, "outcome_unknown", "forge_route_reconcile_unavailable")
previous_phase = record["phase"]
result = _reconcile_locked(
session,
job_id=job_id,
record=record,
npm=route_npm,
defer_phase_write=True,
)
if result["status"] == "host_staged_disabled" and previous_phase != record["phase"]:
if not _save_record(session, record, commit=False):
return _result(job_id, "outcome_unknown", "forge_route_journal_update_failed")
if not _audit(
session,
actor_identity=actor,
decision="allowed",
policy_id=None,
result=result,
idempotency_hash=record["idempotencyKeySha256"],
):
return _result(job_id, "outcome_unknown", "forge_route_audit_persist_failed")
return result
except Exception: # noqa: BLE001 - status remains safe when stores/connectors fail.
return _result(job_id, "outcome_unknown", "forge_route_reconcile_unavailable")
@@ -28,6 +28,7 @@ _COMPOSE_CANDIDATES = (
)
# Must remain identical to DeploymentPlanV1.application_id.
_APP_ID = re.compile(r"^[a-z][a-z0-9-]{1,62}$")
_COMPOSE_PROJECT = re.compile(r"^[a-z0-9][a-z0-9_-]{0,62}$")
_TRANSIENT_STATES = {"gitea_unavailable", "controller_unavailable", "registration_error"}
_RETRY_STATES = _TRANSIENT_STATES | {"controller_conflict", "policy_conflict"}
_DEPLOYMENT_ACTIONS = ("deployment.apply", "deployment.rollback", "deployment.runnerless.provision")
@@ -208,6 +209,7 @@ class GiteaProjectAutoRegistration:
"composeFile",
"composeProject",
"healthTimeout",
"primaryService",
"verifyOciRevision",
}
unknown = set(payload) - allowed
@@ -223,14 +225,18 @@ class GiteaProjectAutoRegistration:
raise ValueError("deployment descriptor applicationId is invalid")
compose_file = str(payload.get("composeFile") or "").strip()
compose_project = str(payload.get("composeProject") or application_id).strip().lower()
if compose_project != application_id:
raise ValueError("deployment descriptor composeProject must equal applicationId")
if not _COMPOSE_PROJECT.fullmatch(compose_project):
raise ValueError("deployment descriptor composeProject is invalid")
primary_service = str(payload.get("primaryService") or application_id).strip()
if not _APP_ID.fullmatch(primary_service):
raise ValueError("deployment descriptor primaryService is invalid")
timeout = int(payload.get("healthTimeout") or 180)
return {
"applicationId": application_id,
"composeFile": compose_file,
"composeProject": compose_project,
"healthTimeout": timeout,
"primaryService": primary_service,
"verifyOciRevision": bool(payload.get("verifyOciRevision", False)),
}
@@ -303,8 +309,9 @@ class GiteaProjectAutoRegistration:
if descriptor_present:
application_id = str(descriptor["applicationId"])
if application_id not in service_names:
raise ValueError("deployment descriptor applicationId must name a Compose service")
primary_service = str(descriptor["primaryService"])
if primary_service not in service_names:
raise ValueError("deployment descriptor primaryService must name a Compose service")
else:
repository_id = self._application_id(repository_name)
compose_name = str(compose.get("name") or "").strip().lower() if isinstance(compose, dict) else ""
@@ -321,8 +328,9 @@ class GiteaProjectAutoRegistration:
# explicit bounded descriptor to select its primary service.
return None
application_id = candidates[0]
primary_service = application_id
compose_project = application_id
compose_project = str(descriptor.get("composeProject") or application_id)
containers: list[str] = []
for service_name, raw_service in services.items():
service = raw_service if isinstance(raw_service, dict) else {}
@@ -334,6 +342,7 @@ class GiteaProjectAutoRegistration:
"applicationId": application_id,
"composeFile": compose_file,
"composeProject": compose_project,
"primaryService": primary_service,
"containers": containers,
"serviceNames": service_names,
"healthTimeout": int(descriptor.get("healthTimeout") or 180),
@@ -510,6 +519,7 @@ class GiteaProjectAutoRegistration:
"applicationId": application_id,
"composeFile": contract["composeFile"],
"composeProject": contract["composeProject"],
"primaryService": contract.get("primaryService", application_id),
"containers": list(contract["containers"]),
"serviceNames": list(contract["serviceNames"]),
}
@@ -337,17 +337,33 @@ def fixed_image_reference(service_name, image_ref):
)
)
if service_name == "postgres":
return image_ref == "postgres:18.4@sha256:3a82e1f56c8f0f5616a11103ac3d47e632c3938698946a7ad26da0df1334744a" or (
image_ref == "postgres:18.6@sha256:0377e72c5289ed2f98cf61b1a9c2db9eb9d300317fe14244492fbc94343b3d04"
and main_declares_external_image(image_ref)
)
if image_ref == "postgres:18.4@sha256:3a82e1f56c8f0f5616a11103ac3d47e632c3938698946a7ad26da0df1334744a":
return True
if image_ref in {
"postgres:18.6@sha256:0377e72c5289ed2f98cf61b1a9c2db9eb9d300317fe14244492fbc94343b3d04",
(
"gitea.example.com/jens/itworx-mcp-hub/postgres:"
"wp400-gosu-v2-20261002-a94e0b4d2029@sha256:"
"a60d81e06987671ae903757f1b71cdfbbff4d481b42610e4de2b7dcfa32aad73"
),
}:
return main_declares_external_image(image_ref)
return False
if service_name == "ingestion-clamd":
return image_ref == "clamav/clamav:1.4.6@sha256:a5f03c12a79dbe9f6d8a527b6bb1ea053fa8dd061d3738a26897f055ee2d9303"
if service_name == "redis":
return image_ref == "redis:8.10.0@sha256:c29e49ab2f85760a3827b53882e6dd9f5c6c3f0bb7d724e07bb31cbf275a5236" or (
image_ref == "redis:8.10.2@sha256:7ef5b5cec96495a04ca7feff88a9492efeab8053fb284d24bdd73344c9245a48"
and main_declares_external_image(image_ref)
)
if image_ref == "redis:8.10.0@sha256:c29e49ab2f85760a3827b53882e6dd9f5c6c3f0bb7d724e07bb31cbf275a5236":
return True
if image_ref in {
"redis:8.10.2@sha256:7ef5b5cec96495a04ca7feff88a9492efeab8053fb284d24bdd73344c9245a48",
(
"gitea.example.com/jens/itworx-mcp-hub/redis:"
"wp400-remediation-20261002-4e18f61c@sha256:"
"7d826b44f245ad32e8fe4bf40d42eae95855c3942c5f2e42784ee8fd3f9547a2"
),
}:
return main_declares_external_image(image_ref)
return False
if service_name == "contextforge":
return main_declares_contextforge_compat_image(
"itworx-mcp-hub-contextforge-1", image_ref
+23
View File
@@ -187,6 +187,29 @@ def evaluate_policy(
"scope_decision": "allowed" if allowed else None,
}
if action == "forge.route.stage":
# This narrowly scoped external-route mutation never inherits the
# generic manual fallback or a wildcard grant. The row is seeded
# blocked and must be explicitly enabled for the one Forge target.
policies = session.exec(select(Policy).where(Policy.action == action)).all()
policy = policies[0] if len(policies) == 1 else None
allowed = bool(
source == "manual"
and target == "itworx-forge"
and policy is not None
and policy.target == "itworx-forge"
and policy.manual == "allowed"
and policy.voice == "blocked"
and policy.risk == "critical"
)
return {
"decision": "allowed" if allowed else "blocked",
"reason": "Forge route staging requires one exact-target manual policy; voice is always blocked.",
"risk": "critical",
"policy_id": policy.id if allowed and policy is not None else None,
"scope_decision": "allowed" if allowed else None,
}
if action in _DEFAULT_DECISIONS:
reason = _DEFAULT_REASONS.get(action, 'Action uses default governance policy.')
return {