Publish curated Unraid-AppOps-Gateway source from ad0a390e431166c3198a2ac3bec010c2b44f2dea

This commit is contained in:
Public Source Publisher committed 2026-10-02 09:56:13 +00:00
1 parent 29c1e3d5a1
commit 98d31477f9
35 files changed
+7264 -185

No files matched your search

+10 -6
View File
@@ -7,14 +7,15 @@ from fastapi import APIRouter, Depends, Header, HTTPException
from pydantic import BaseModel, ConfigDict, Field
from sqlmodel import Session, select
from ..db.session import get_session
from ..db.models import App, AppSetting, utc_now
from ..services.audit_logger import log_event
from ..services.container_restart_service import execute_container_restart
from ..services.policy_engine import evaluate_policy
from ..db.session import get_session
from ..services.action_execution import (
action_execution_profile,
execute_governed_action,
)
from ..services.action_registry import (
ACTIONS,
ACTION_RISK,
ACTIONS,
action_blast_radius,
action_capability,
action_category,
@@ -24,7 +25,9 @@ from ..services.action_registry import (
action_safeguards,
)
from ..services.approval_service import create_approval
from ..services.action_execution import action_execution_profile, execute_governed_action
from ..services.audit_logger import log_event
from ..services.container_restart_service import execute_container_restart
from ..services.policy_engine import evaluate_policy
from ..services.preflight_service import build_preflight
router = APIRouter(prefix='/api/actions')
@@ -446,6 +449,7 @@ def execute(payload: ActionRequest, session: Session = Depends(get_session), ide
})
if action in {
'controller.managed-storage.initialize',
'deployment.targets.sync',
'deployment.target.deploy_workmesh',
'deployment.target.register_portfolio',
+166 -3
View File
@@ -2,20 +2,37 @@
from __future__ import annotations
from copy import deepcopy
from typing import Annotated, Literal
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request
from pydantic import BaseModel, ConfigDict
from sqlalchemy import insert as sql_insert
from sqlalchemy import update as sql_update
from sqlalchemy.exc import SQLAlchemyError
from sqlmodel import Session
from ..core.security import require_operator_request
from ..core.time import utc_now
from ..db.models import AppSetting
from ..db.session import get_session
from ..schemas.enrollment import EnrollmentPlanV1, EnrollmentTrustConfigV1
from ..services.enrollment_plane import EnrollmentError, EnrollmentPlane
from ..schemas.enrollment import (
LEGACY_APPDATA_ROOT,
EnrollmentPlanV1,
EnrollmentStorageRootSelectionV1,
EnrollmentTrustConfigV1,
)
from ..services.audit_logger import log_event
from ..services.enrollment_plane import (
TRUST_SETTING_KEY,
EnrollmentError,
EnrollmentPlane,
)
from ..services.repopulse_adoption_journal import (
RepoPulseAdoptionJournalError,
get_repopulse_adoption_status,
)
from ..services.user_access import current_user
def _require_operator(request: Request) -> str:
@@ -58,10 +75,156 @@ def enrollment_trust_status(session: EnrollmentSession):
@router.put("/trust")
def update_enrollment_trust(payload: EnrollmentTrustConfigV1, session: EnrollmentSession):
def update_enrollment_trust(
payload: EnrollmentTrustConfigV1,
session: EnrollmentSession,
request: Request,
):
actor = current_user(request)
if actor.role != "admin":
row = session.get(AppSetting, TRUST_SETTING_KEY)
original_value = deepcopy(row.value_json) if row is not None else None
original_updated_at = row.updated_at if row is not None else None
if row is None or row.is_secret:
existing = None
else:
try:
existing = EnrollmentTrustConfigV1.model_validate(original_value)
except ValueError:
existing = None
if row is None:
# Preserve initial legacy setup for settings.manage users, but the
# fixed managed-root opt-in must be made by an administrator.
selector_changed = (
payload.trusted_appdata_root != LEGACY_APPDATA_ROOT
or payload.managed_appdata_backup_acknowledged is not False
)
elif existing is None:
# A corrupt stored trust document has no safe selector baseline.
selector_changed = True
else:
selector_changed = (
payload.trusted_appdata_root != existing.trusted_appdata_root
or payload.managed_appdata_backup_acknowledged
!= existing.managed_appdata_backup_acknowledged
)
if selector_changed:
raise HTTPException(
status_code=403,
detail="Administrator role is required to change the appdata root.",
)
try:
if row is None:
# A concurrent administrator may create the trust row after the
# absence check. The unique key makes this insert a safe CAS:
# losing the race is a conflict, never an overwrite.
session.execute(
sql_insert(AppSetting).values(
key=TRUST_SETTING_KEY,
value_json=payload.model_dump(mode="json"),
is_secret=False,
updated_at=utc_now(),
)
)
else:
result = session.execute(
sql_update(AppSetting)
.where(AppSetting.key == TRUST_SETTING_KEY)
.where(AppSetting.updated_at == original_updated_at)
.where(AppSetting.value_json == original_value)
.values(
value_json=payload.model_dump(mode="json"),
is_secret=False,
updated_at=utc_now(),
)
.execution_options(synchronize_session=False)
)
if result.rowcount != 1:
session.rollback()
raise HTTPException(status_code=409, detail="enrollment_trust_changed")
session.commit()
except HTTPException:
raise
except SQLAlchemyError:
session.rollback()
raise HTTPException(status_code=409, detail="enrollment_trust_changed") from None
session.expire_all()
return EnrollmentPlane.trust_status(session)
return EnrollmentPlane.set_trust_config(session, payload)
@router.patch("/trust/storage-root")
def update_enrollment_storage_root(
payload: EnrollmentStorageRootSelectionV1,
session: EnrollmentSession,
request: Request,
):
"""Change only the fixed storage selector and persist its backup acknowledgement."""
if current_user(request).role != "admin":
raise HTTPException(status_code=403, detail="Administrator role is required.")
trust = EnrollmentPlane.get_trust_config(session)
row = session.get(AppSetting, TRUST_SETTING_KEY)
if trust is None or row is None:
raise HTTPException(status_code=409, detail="enrollment_trust_not_configured")
original_value = deepcopy(row.value_json)
original_updated_at = row.updated_at
try:
stored_trust = EnrollmentTrustConfigV1.model_validate(original_value)
except ValueError:
raise HTTPException(status_code=409, detail="enrollment_trust_changed") from None
if stored_trust.model_dump(mode="json") != trust.model_dump(mode="json"):
raise HTTPException(status_code=409, detail="enrollment_trust_changed")
updated = EnrollmentTrustConfigV1.model_validate({
**original_value,
**payload.model_dump(mode="json"),
})
before = {
"trustedAppdataRoot": trust.trusted_appdata_root,
"managedAppdataBackupAcknowledged": trust.managed_appdata_backup_acknowledged,
}
after = {
"trustedAppdataRoot": updated.trusted_appdata_root,
"managedAppdataBackupAcknowledged": updated.managed_appdata_backup_acknowledged,
}
try:
result = session.execute(
sql_update(AppSetting)
.where(AppSetting.key == TRUST_SETTING_KEY)
.where(AppSetting.updated_at == original_updated_at)
.where(AppSetting.value_json == original_value)
.values(
value_json=updated.model_dump(mode="json"),
is_secret=False,
updated_at=utc_now(),
)
.execution_options(synchronize_session=False)
)
except SQLAlchemyError:
session.rollback()
raise HTTPException(status_code=409, detail="enrollment_trust_changed") from None
if result.rowcount != 1:
session.rollback()
raise HTTPException(status_code=409, detail="enrollment_trust_changed")
session.expire(row)
log_event(
session,
source="settings",
identity="manual",
action_id="enrollment.trust.storage_root.update",
target="tower/deployment-controller",
risk="high",
policy_decision="allowed",
reason="Authenticated settings.manage operator selected a fixed appdata root and recorded its backup acknowledgement.",
result="updated",
target_type="controller",
before_state=before,
after_state=after,
redacted_metadata={"backupLocation": "dockerRootDir" if updated.managed_appdata_backup_acknowledged else None},
)
return EnrollmentPlane.trust_status(session)
@router.post("")
def request_enrollment(
payload: EnrollmentRequestV1,
+8 -2
View File
@@ -34,7 +34,9 @@ _SAFE_PATH = re.compile(
r"^(?!/)(?!.*(?:^|/)\.\.(?:/|$))(?:[A-Za-z0-9]|\.[A-Za-z0-9])[A-Za-z0-9_./-]{0,239}$"
)
TRUSTED_APPCONFIG_ROOT = "/srv/example/appdata"
LEGACY_APPCONFIG_ROOT = "/srv/example/appdata"
MANAGED_APPCONFIG_ROOT = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
TRUSTED_APPCONFIG_ROOTS = (LEGACY_APPCONFIG_ROOT, MANAGED_APPCONFIG_ROOT)
class _StrictModel(BaseModel):
@@ -177,6 +179,10 @@ class DeploymentPlanV1(_StrictModel):
image_repository: str
image_tag: str
topology: DeploymentTopologyV1
trusted_appdata_root: Literal[
"/srv/example/appdata",
"/var/lib/docker/volumes/itworx-managed-appdata/_data",
] = LEGACY_APPCONFIG_ROOT
proxy: ProxyHostV1 | None = None
oidc: OidcProvisioningV1 | None = None
health_path: str = "/healthz"
@@ -257,7 +263,7 @@ class DeploymentPlanV1(_StrictModel):
@property
def appdata_path(self) -> str:
return f"{TRUSTED_APPCONFIG_ROOT}/{self.application_id}"
return f"{self.trusted_appdata_root}/{self.application_id}"
@property
def public_url(self) -> str | None:
+37 -3
View File
@@ -29,6 +29,9 @@ DeploymentProfileName = Literal[
"repopulse-authenticated",
]
LEGACY_APPDATA_ROOT = "/srv/example/appdata"
MANAGED_DOCKER_APPDATA_ROOT = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
class _StrictModel(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
@@ -218,6 +221,10 @@ class EnrollmentPlanV1(_StrictModel):
storage: EnrollmentStorageV1
network: EnrollmentNetworkV1
artifact: EnrollmentArtifactV1
trusted_appdata_root: Literal[
"/srv/example/appdata",
"/var/lib/docker/volumes/itworx-managed-appdata/_data",
] = "/srv/example/appdata"
exposure: EnrollmentExposureV1 | None = None
authentication: EnrollmentAuthenticationV1 = EnrollmentAuthenticationV1()
secrets: EnrollmentSecretsV1 = EnrollmentSecretsV1()
@@ -271,7 +278,8 @@ class EnrollmentPlanV1(_StrictModel):
if expects_oidc != self.secrets.oidc_client_secret_required:
raise ValueError("OIDC secret requirement must match the selected profile")
if self.profile == "repopulse-authenticated" and (
self.application_id != "repopulse"
self.trusted_appdata_root != "/srv/example/appdata"
or self.application_id != "repopulse"
or self.repository != "jens/repopulse"
or self.environment != "production"
or self.target.container_name != "repopulse"
@@ -295,7 +303,7 @@ class EnrollmentPlanV1(_StrictModel):
@property
def appdata_path(self) -> str:
return f"/srv/example/appdata/{self.application_id}"
return f"{self.trusted_appdata_root}/{self.application_id}"
@property
def data_path(self) -> str:
@@ -311,7 +319,11 @@ class EnrollmentTrustConfigV1(_StrictModel):
controller_identity: str
bootstrap_secret_ref: str
trusted_appdata_root: Literal["/srv/example/appdata"] = "/srv/example/appdata"
trusted_appdata_root: Literal[
LEGACY_APPDATA_ROOT,
MANAGED_DOCKER_APPDATA_ROOT,
] = LEGACY_APPDATA_ROOT
managed_appdata_backup_acknowledged: bool = Field(default=False, strict=True)
approved_networks: tuple[str, ...] = ()
allowed_domain_suffixes: tuple[str, ...] = ()
autonomous_profiles: tuple[DeploymentProfileName, ...] = ()
@@ -372,4 +384,26 @@ class EnrollmentTrustConfigV1(_StrictModel):
for group in groups:
if group is not None and not re.fullmatch(r"[A-Za-z0-9 _.-]{2,100}", group):
raise ValueError("OIDC role group name is invalid")
managed_root = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
if self.trusted_appdata_root == managed_root and not self.managed_appdata_backup_acknowledged:
raise ValueError("managed Docker-root storage requires explicit backup acknowledgement")
if self.trusted_appdata_root != managed_root and self.managed_appdata_backup_acknowledged:
raise ValueError("Docker-root backup acknowledgement is valid only for the managed root")
return self
class EnrollmentStorageRootSelectionV1(_StrictModel):
"""Narrow operator selection for the fixed managed storage root."""
trusted_appdata_root: Literal[
LEGACY_APPDATA_ROOT,
MANAGED_DOCKER_APPDATA_ROOT,
]
managed_appdata_backup_acknowledged: bool = Field(strict=True)
@model_validator(mode="after")
def require_managed_backup_acknowledgement(self) -> EnrollmentStorageRootSelectionV1:
managed = self.trusted_appdata_root == MANAGED_DOCKER_APPDATA_ROOT
if managed != self.managed_appdata_backup_acknowledged:
raise ValueError("managed Docker-root storage requires explicit backup acknowledgement")
return self
+180 -2
View File
@@ -21,6 +21,7 @@ from .setup_service import apply_stored_connector_settings
GENERIC_EXECUTOR_ACTIONS = {
"container.restart",
"controller.artifact.promote",
"controller.managed-storage.initialize",
"deployment.targets.sync",
"deployment.runnerless.provision",
"deployment.target.deploy_workmesh",
@@ -69,6 +70,10 @@ APPROVAL_EXECUTOR_ACTIONS = {
"unraid.vm.pause",
}
MCP_TYPED_EXECUTOR_ACTIONS = APPROVAL_EXECUTOR_ACTIONS | {
"controller.managed-storage.initialize",
}
HUMAN_APPROVAL_ONLY_ACTIONS = {"deployment.target.promote_workmesh", "privatemuse.comfyui.install_artifact"}
_APPROVAL_STEPS = {
@@ -148,7 +153,7 @@ def approval_execution_supported(action: str) -> bool:
def mcp_execution_supported(action: str) -> bool:
"""MCP is a no-human-approval surface; never admit human-only executors."""
return approval_execution_supported(action) and action not in HUMAN_APPROVAL_ONLY_ACTIONS
return action in MCP_TYPED_EXECUTOR_ACTIONS and action not in HUMAN_APPROVAL_ONLY_ACTIONS
def _execute_deployment_target_sync(
@@ -1758,7 +1763,7 @@ def execute_governed_action(
small allowlist of typed rollback/post-check capable executors below is reachable.
"""
if not approval_execution_supported(action):
if not mcp_execution_supported(action):
return {
"success": False,
"status": "failed",
@@ -1786,6 +1791,24 @@ def execute_governed_action(
original_policy_decision=original_policy_decision,
)
if action == "controller.managed-storage.initialize":
if target != "tower/deployment-controller":
return {
"success": False,
"status": "failed",
"result": "blocked",
"message": "Managed storage initialization accepts only tower/deployment-controller.",
"policyDecision": "blocked",
"originalPolicyDecision": original_policy_decision,
}
return _execute_managed_appdata_initialize(
session,
source=source,
risk=risk,
reason=reason,
original_policy_decision=original_policy_decision,
)
if action == "runner.fleet.upgrade_v2":
if target.strip().lower() != "tower/runner-fleet-v2":
return {
@@ -2058,6 +2081,161 @@ def execute_governed_action(
}
def _execute_managed_appdata_initialize(
session: Session,
*,
source: str,
risk: str,
reason: str,
original_policy_decision: str,
) -> dict[str, Any]:
"""Initialize only the fixed storage marker after trust and policy opt-in."""
from .deployment_broker import DeploymentBrokerClient
from .enrollment_plane import EnrollmentPlane
from .secret_store import get_secret
action = "controller.managed-storage.initialize"
target = "tower/deployment-controller"
trust = EnrollmentPlane.get_trust_config(session)
if (
trust is None
or trust.trusted_appdata_root != "/var/lib/docker/volumes/itworx-managed-appdata/_data"
or trust.managed_appdata_backup_acknowledged is not True
):
audit = log_event(
session,
source="action",
identity=source,
action_id=action,
target=target,
target_type="controller",
risk=risk,
policy_decision="allowed",
reason="Managed Docker-root storage requires an explicit trusted selector and backup acknowledgement.",
result="blocked",
redacted_metadata={"executor": action, "trustOptIn": False},
)
return {
"success": False,
"status": "failed",
"result": "blocked",
"message": "Managed storage is not explicitly selected and acknowledged in enrollment trust.",
"auditId": audit.id,
"policyDecision": "allowed",
"originalPolicyDecision": original_policy_decision,
}
bootstrap_secret = get_secret(session, trust.bootstrap_secret_ref, touch=False)
if not bootstrap_secret:
audit = log_event(
session,
source="action",
identity=source,
action_id=action,
target=target,
target_type="controller",
risk=risk,
policy_decision="allowed",
reason="Controller bootstrap capability is unavailable by its configured reference.",
result="failed",
redacted_metadata={"executor": action, "secretMaterialReturned": False},
)
return {
"success": False,
"status": "failed",
"result": "failed",
"message": "Controller bootstrap capability is unavailable.",
"auditId": audit.id,
"policyDecision": "allowed",
"originalPolicyDecision": original_policy_decision,
}
steps = [
"Verify the exact target, trust selector and backup acknowledgement",
"Authenticate the fixed controller request with bootstrap HMAC and replay protection",
"Validate the protected controller, target registry, repository root and fixed Docker volume",
"Initialize the selector only if the managed Docker volume is empty and unchanged",
"Re-read the fixed selector and record sanitized controller readiness",
]
job = create_job(
session,
action_id=action,
target=target,
source=source,
risk=risk,
policy_decision="allowed",
steps=steps,
)
start_job(session, job)
set_job_progress(session, job, 20, "Validating fixed managed-storage initialization prerequisites.")
try:
result = DeploymentBrokerClient().initialize_managed_appdata(secret=bootstrap_secret)
reported_status = result.get("status")
status = (
reported_status
if isinstance(reported_status, str)
and reported_status in {"initialized", "already_initialized"}
else "refused"
)
success = (
status in {"initialized", "already_initialized"}
and result.get("trustedAppdataRoot") == trust.trusted_appdata_root
and result.get("backupLocation") == "dockerRootDir"
)
message = (
"Managed storage selector initialized and verified."
if success
else "Managed storage initialization was refused or unavailable."
)
except Exception:
result = {"status": "error"}
status = "error"
success = False
message = "Managed storage initialization was refused or unavailable."
completed = complete_job(
session,
job,
status="succeeded" if success else "failed",
result_summary=message,
)
audit = log_event(
session,
source="action",
identity=source,
action_id=action,
target=target,
target_type="controller",
risk=risk,
policy_decision="allowed",
reason=reason,
result="success" if success else "failed",
execution_plan={"steps": steps, "jobId": completed.id},
after_state={
"status": status,
"trustedAppdataRoot": trust.trusted_appdata_root if success else None,
"backupLocation": "dockerRootDir" if success else None,
},
redacted_metadata={
"executor": action,
"target": target,
"integration": "mcp" if source == "chatgpt-mcp" else "operator",
"originalPolicyDecision": original_policy_decision,
"secretMaterialReturned": False,
},
)
return {
"success": success,
"status": "success" if success else "failed",
"result": "success" if success else "failed",
"message": message,
"jobId": completed.id,
"auditId": audit.id,
"policyDecision": "allowed",
"originalPolicyDecision": original_policy_decision,
}
def execute_approved_action(session: Session, approval: Approval) -> dict[str, Any]:
if not approval_execution_supported(approval.action):
return {
+4
View File
@@ -8,6 +8,7 @@ ACTIONS = [
'container.stop',
'runner.fleet.upgrade_v2',
'controller.artifact.promote',
'controller.managed-storage.initialize',
'gateway.clear_cache',
'stack.status',
'stack.redeploy',
@@ -214,6 +215,7 @@ ACTION_RISK = {
'container.stop': 'high',
'runner.fleet.upgrade_v2': 'high',
'controller.artifact.promote': 'high',
'controller.managed-storage.initialize': 'high',
'gateway.clear_cache': 'high',
'stack.status': 'low',
'stack.redeploy': 'high',
@@ -525,6 +527,8 @@ def action_category(action: str) -> str:
def action_default_target(action: str) -> str:
if action == 'controller.artifact.promote':
return 'tower/deployment-controller'
if action == 'controller.managed-storage.initialize':
return 'tower/deployment-controller'
if action == 'runner.fleet.upgrade_v2':
return 'tower/runner-fleet-v2'
if action == 'deployment.targets.sync':
@@ -39,12 +39,12 @@ def helper_container_spec(*, revision: str, image_id: str) -> dict[str, Any]:
raise ValueError("controller_promoter_image_invalid")
command = f"""set -Eeuo pipefail
umask 077
revision='{revision}'
version='{CONTROLLER_VERSION}'
repo='/gitea-repositories/jens/unraid-appops-gateway.git'
work='/mnt/cache/appdata/gitea-deploy-runners/.controller-promoter-{revision[:12]}'
rm -rf -- "$work"
install -d -m 0700 "$work"
work="$(mktemp -d -p /tmp 'itworx-controller-artifact-promoter-{revision[:12]}.XXXXXX')"
[[ -d "$work" && ! -L "$work" && "$(stat -c '%u:%a' "$work")" == '0:700' ]]
cleanup() {{ rm -rf -- "$work"; }}
trap cleanup EXIT
command -v git >/dev/null
@@ -59,7 +59,10 @@ git --git-dir="$repo" archive --format=tar "$revision" \
ops/gitea-deploy-broker \
scripts/prepare_controller_artifact.py \
scripts/reconcile_controller_state.py \
scripts/run_repopulse_adoption.py > "$work/controller.tar"
scripts/run_repopulse_adoption.py \
scripts/prepare_controller_hub_release_volume.py \
scripts/upgrade_deployment_controller.sh > "$work/controller.tar"
chmod 0600 "$work/controller.tar"
archive_sha="$(sha256sum "$work/controller.tar" | cut -d ' ' -f 1)"
git --git-dir="$repo" show "$revision:scripts/upgrade_deployment_controller.sh" > "$work/upgrade.sh"
chmod 0700 "$work/upgrade.sh"
@@ -70,6 +73,7 @@ bash "$work/upgrade.sh" \
--source-revision "$revision" \
--source-tree "$tree" \
--controller-version "$version" \
--image-id '{image_id}' \
--confirm UPGRADE_DEPLOYMENT_CONTROLLER_V2
"""
@@ -88,6 +92,7 @@ bash "$work/upgrade.sh" \
"/mnt/cache:/mnt/cache",
"/mnt/user:/mnt/user",
"/srv/example/appdata/gitea/git/repositories:/gitea-repositories:ro",
"/var/lib/docker:/var/lib/docker:ro",
"/var/run/docker.sock:/var/run/docker.sock",
],
"NetworkMode": "host",
+12
View File
@@ -311,6 +311,18 @@ class DeploymentBrokerClient:
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(
+123 -4
View File
@@ -22,7 +22,12 @@ from ..connectors.nginx_proxy_manager import NginxProxyManagerConnector
from ..connectors.portainer import PortainerConnector
from ..core.time import utc_now
from ..db.models import AppSetting, Enrollment, EnrollmentJob, Policy
from ..schemas.enrollment import EnrollmentPlanV1, EnrollmentTrustConfigV1
from ..schemas.enrollment import (
LEGACY_APPDATA_ROOT,
MANAGED_DOCKER_APPDATA_ROOT,
EnrollmentPlanV1,
EnrollmentTrustConfigV1,
)
from .audit_logger import log_event
from .deployment_broker import DeploymentBrokerClient
from .enrollment_profiles import PROFILE_VERSION, profile_contract, profile_summary
@@ -40,6 +45,47 @@ _TERMINAL = {"ready", "failed", "compensation_required", "preflight_failed"}
_CONTROLLER_EVIDENCE = {"status", "targetId", "repository", "applicationId", "appdataPath"}
_PROXY_EVIDENCE = {"success", "status", "hostId", "hostname"}
_OIDC_EVIDENCE = {"success", "status", "applicationId", "providerId", "clientSecretCreated"}
_APPDATA_ROOT_STATUSES = {"ready", "needs_initialization", "unavailable"}
def _trusted_appdata_roots_readiness(
controller_capability: dict[str, Any],
) -> list[dict[str, str]]:
roots = (LEGACY_APPDATA_ROOT, MANAGED_DOCKER_APPDATA_ROOT)
unavailable = {root: "unavailable" for root in roots}
reported = controller_capability.get("availableTrustedAppdataRoots")
if isinstance(reported, list):
if len(reported) != len(roots):
return [{"root": root, "status": unavailable[root]} for root in roots]
statuses: dict[str, str] = {}
for item in reported:
if not isinstance(item, dict):
return [{"root": root, "status": unavailable[root]} for root in roots]
root = item.get("root")
status = item.get("status")
if (
not isinstance(root, str)
or root not in unavailable
or root in statuses
or not isinstance(status, str)
or status not in _APPDATA_ROOT_STATUSES
):
return [{"root": expected, "status": unavailable[expected]} for expected in roots]
statuses[root] = status
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]
# A legacy V1 controller reports only its configured root. It cannot attest
# independent managed-volume readiness, so only its exact legacy root can
# be marked ready through this compatibility path.
if "availableTrustedAppdataRoots" not in controller_capability:
if (
controller_capability.get("status") == "ready"
and controller_capability.get("trustedAppdataRoot") == LEGACY_APPDATA_ROOT
):
unavailable[LEGACY_APPDATA_ROOT] = "ready"
return [{"root": root, "status": unavailable[root]} for root in roots]
class EnrollmentError(ValueError):
@@ -135,6 +181,8 @@ class EnrollmentPlane:
"status": "needs_setup",
"blockers": ["enrollment_trust_not_configured"],
"configured": False,
"trustedAppdataRoot": LEGACY_APPDATA_ROOT,
"managedAppdataBackupAcknowledged": False,
"secretStore": storage,
}
return {
@@ -143,6 +191,7 @@ class EnrollmentPlane:
"configured": True,
"controllerIdentity": trust.controller_identity,
"trustedAppdataRoot": trust.trusted_appdata_root,
"managedAppdataBackupAcknowledged": trust.managed_appdata_backup_acknowledged,
"approvedNetworks": list(trust.approved_networks),
"allowedDomainSuffixes": list(trust.allowed_domain_suffixes),
"autonomousProfiles": list(trust.autonomous_profiles),
@@ -162,11 +211,43 @@ class EnrollmentPlane:
bootstrap_secret = get_secret(session, trust.bootstrap_secret_ref, touch=False)
if bootstrap_secret:
controller_capability = self.broker.enrollment_capabilities(secret=bootstrap_secret)
available_roots = _trusted_appdata_roots_readiness(controller_capability)
if trust is not None:
selected_root = next(
(
item
for item in available_roots
if item["root"] == trust.trusted_appdata_root
),
None,
)
controller_root_matches = bool(selected_root and selected_root.get("status") == "ready")
else:
controller_root_matches = False
broker_ready = bool(
bootstrap
and self.broker.is_configured()
and controller_capability.get("status") == "ready"
and controller_root_matches
)
controller_managed_storage = controller_capability.get("managedStorage")
if not isinstance(controller_managed_storage, dict):
controller_managed_storage = {}
if trust is None or trust.trusted_appdata_root != MANAGED_DOCKER_APPDATA_ROOT:
managed_storage_status = "not_selected"
managed_storage_blocker = ""
elif not trust.managed_appdata_backup_acknowledged:
managed_storage_status = "blocked"
managed_storage_blocker = "managed_appdata_backup_acknowledgement_required"
elif controller_root_matches and controller_managed_storage.get("status") == "ready":
managed_storage_status = "ready"
managed_storage_blocker = ""
elif controller_managed_storage.get("initializationAvailable") is True:
managed_storage_status = "initialization_required"
managed_storage_blocker = "managed_appdata_initialization_required"
else:
managed_storage_status = "unavailable"
managed_storage_blocker = "managed_appdata_volume_unavailable"
npm_ready = self.npm.is_configured()
oidc_configured = bool(
trust
@@ -193,9 +274,27 @@ class EnrollmentPlane:
"policyProvisioning": item(configured, "policy_template_unavailable"),
"rollback": item(broker_ready, "deployment_executor_unavailable"),
"verification": item(broker_ready, "deployment_executor_unavailable"),
"availableTrustedAppdataRoots": available_roots,
"managedStorage": {
"status": managed_storage_status,
"blockerCode": managed_storage_blocker,
"root": MANAGED_DOCKER_APPDATA_ROOT,
"backupScope": "dockerRootDir",
"backupAcknowledged": bool(
trust and trust.managed_appdata_backup_acknowledged
),
},
"controller": {
"status": str(controller_capability.get("status") or "unavailable"),
"reason": str(controller_capability.get("reason") or "deployment_executor_unavailable"),
"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"
))
),
},
"profiles": [profile_summary(name) for name in ("internal-webapp", "internal-webapp-oidc", "internal-service", "repopulse-authenticated")],
}
@@ -233,6 +332,7 @@ class EnrollmentPlane:
"container_name": plan.target.container_name,
"compose_file": plan.target.compose_file,
"appdata_path": plan.appdata_path,
"trusted_appdata_root": plan.trusted_appdata_root,
"data_path": plan.data_path,
"runtime_uid": plan.storage.runtime_uid,
"runtime_gid": plan.storage.runtime_gid,
@@ -264,6 +364,17 @@ class EnrollmentPlane:
checks.append(_check("network", "passed", "Requested Docker network is within the trusted profile boundary."))
else:
checks.append(_check("network_unavailable", "blocked", "Requested Docker network is not approved for enrollment."))
# RepoPulse is an established, fixed legacy-root exception; the
# operator's selected root applies to other new targets only.
expected_appdata_root = (
LEGACY_APPDATA_ROOT
if plan.profile == "repopulse-authenticated"
else trust.trusted_appdata_root
)
if plan.trusted_appdata_root == expected_appdata_root:
checks.append(_check("trusted_appdata_root", "passed", "Requested appdata root matches its fixed profile trust boundary."))
else:
checks.append(_check("trusted_appdata_root_mismatch", "blocked", "Requested appdata root does not match the explicit operator trust boundary."))
if plan.profile in trust.autonomous_profiles:
checks.append(_check("autonomous_profile", "passed", "Deployment profile is approved for autonomous enrollment."))
else:
@@ -397,7 +508,15 @@ class EnrollmentPlane:
checks.append(_check("secret_store", "passed" if storage_ready else "blocked", "Production-grade write-only secret storage is ready." if storage_ready else "Enrollment requires APPOPS_SECRET_KEY from a protected environment variable or file; the development fallback is not permitted."))
if trust:
checks.append(_check("controller_bootstrap_secret", "passed" if has_secret(session, trust.bootstrap_secret_ref) else "blocked", "Controller bootstrap secret is available by reference." if has_secret(session, trust.bootstrap_secret_ref) else "Controller bootstrap secret reference is not configured."))
if has_secret(session, trust.bootstrap_secret_ref):
expected_appdata_root = (
LEGACY_APPDATA_ROOT
if plan.profile == "repopulse-authenticated"
else trust.trusted_appdata_root
)
root_matches_trust = plan.trusted_appdata_root == expected_appdata_root
if not root_matches_trust:
checks.append(_check("controller_target", "blocked", "Controller preflight was skipped because the requested appdata root is outside the profile trust boundary."))
elif has_secret(session, trust.bootstrap_secret_ref):
controller = self.broker.enrollment_preflight(
self._controller_payload(plan, enrollment_id="enr-00000000000000000000"),
secret=get_secret(session, trust.bootstrap_secret_ref, touch=False) or "",
@@ -10,6 +10,7 @@ from ..core.redaction import redact_message
_CONTROLLER = "gitea-deploy-control"
_REPOSITORY = "jens/itworx_mcp_hub"
_COMPOSE_PROJECT = "itworx-mcp-hub"
_TOPOLOGY_CONTRACT = "itworx.mcp-hub-topology/17-v1"
# One-time fail-closed re-attestation for the existing MCP Hub runtime. The
# script never builds, restarts, recreates, stops or deploys a container. It
@@ -28,11 +29,12 @@ import sys
import time
repository, expected_compose_project = sys.argv[1:3]
registry_path = Path("/opt/gitea-deploy/projects.json")
host_registry_path = Path("/mnt/cache/appdata/gitea-deploy-runners/controller/projects.json")
registry_path = Path("/controller-config/projects.json")
host_registry_path = Path("/controller-hub-releases/controller-config/projects.json")
state_root = Path("/mnt/cache/appdata/gitea-deploy-runners/state")
global_lock_path = Path("/mnt/cache/appdata/gitea-deploy-runners/deploy.lock")
owned_image_source = "https://gitea.example.com/jens/itworx-mcp-hub"
topology_contract = "itworx.mcp-hub-topology/17-v1"
if repository != "jens/itworx_mcp_hub" or expected_compose_project != "itworx-mcp-hub":
raise SystemExit(40)
@@ -69,6 +71,10 @@ expected_fixed = {
"itworx-mcp-hub-web-1",
"itworx-mcp-hub-worker-1",
}
expected_runtime = expected_fixed | {
"itworx-mcp-hub-connector-ingestion-1",
"itworx-mcp-hub-ingestion-clamd-1",
}
if (
project.get("branch") != "main"
or project.get("slug") != expected_compose_project
@@ -77,6 +83,7 @@ if (
or set(expected_containers) != expected_fixed
):
raise SystemExit(23)
expected_containers = sorted(expected_runtime)
state_path = state_root / f"{expected_compose_project}.json"
project_lock_path = state_root / "locks" / f"{expected_compose_project}.lock"
@@ -310,12 +317,33 @@ with project_lock:
receipt = state.get("live_receipt")
recorded_artifacts = state.get("live_artifacts")
registered_live_containers = (
recorded_artifacts.get("containers")
if isinstance(recorded_artifacts, dict)
else None
)
if isinstance(registered_live_containers, dict) and set(registered_live_containers) == expected_fixed:
print("mcp-hub-topology-migration-required", flush=True)
raise SystemExit(56)
already_reconciled = (
state.get("live_legacy_provenance") is True
and recorded_status == "success"
recorded_status == "success"
and recorded_live_status == "healthy"
and re.fullmatch(r"[0-9a-f]{40}", recorded_live) is not None
and isinstance(recorded_artifacts, dict)
and state.get("compose_project") == expected_compose_project
and isinstance(registered_live_containers, dict)
and set(registered_live_containers) == expected_runtime
and recorded_artifacts.get("topology_contract") == topology_contract
and (
state.get("live_legacy_provenance") is True
or (
isinstance(receipt, dict)
and receipt.get("schema") == "gitea-deploy-receipt.v1"
and receipt.get("repository") == repository
and receipt.get("revision") == recorded_live
)
)
)
if not already_reconciled and (
recorded_status != "failed"
@@ -513,7 +541,11 @@ with project_lock:
if not drift_found:
raise SystemExit(52)
artifacts = {"containers": container_artifacts, "compose_sha256": ""}
artifacts = {
"containers": container_artifacts,
"compose_sha256": "",
"topology_contract": topology_contract,
}
now = int(time.time())
payload = {
"schema_version": 3,
@@ -678,8 +710,16 @@ class McpHubDeploymentReconciliation:
53: "reconciled MCP Hub deployment state could not be written durably",
54: "reconciled MCP Hub deployment state did not verify after write",
55: "unknown MCP Hub live status was not produced by a proven recoverable snapshot-stage failure",
56: "MCP Hub 15-to-17 topology migration requires its guarded protected-main deployment",
}
if exit_code != 0:
if exit_code == 56:
return {
"status": "topology_migration_required",
"repository": _REPOSITORY,
"composeProject": _COMPOSE_PROJECT,
"reason": reasons[exit_code],
}
return {
"status": "validation_failed",
"reason": reasons.get(exit_code, "MCP Hub deployment reconciliation failed closed"),
+22
View File
@@ -142,6 +142,28 @@ def evaluate_policy(
'scope_decision': None,
}
if action == "controller.managed-storage.initialize":
exact_policy = session.exec(
select(Policy).where(
(Policy.action == action) & (Policy.target == "tower/deployment-controller")
)
).first()
allowed = bool(
target == "tower/deployment-controller"
and source != "voice"
and exact_policy is not None
and exact_policy.manual == "allowed"
)
return {
"decision": "allowed" if allowed else "blocked",
"reason": (
"Managed storage initialization requires an exact-target explicit operator policy."
),
"risk": ACTION_RISK.get(action, "high"),
"policy_id": exact_policy.id if allowed and exact_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 {
+6 -1
View File
@@ -170,7 +170,12 @@ def required_permission(path: str, method: str) -> str:
return "settings.manage"
if path.startswith(("/api/settings/connectors", "/api/connectors", "/api/setup/")):
return "connectors.manage"
if path.startswith(("/api/settings/", "/api/system/")) or path in {"/api/ai/rag/reindex", "/api/knowledge/reindex", "/api/enrollments/trust"}:
if path.startswith(("/api/settings/", "/api/system/")) or path in {
"/api/ai/rag/reindex",
"/api/knowledge/reindex",
"/api/enrollments/trust",
"/api/enrollments/trust/storage-root",
}:
return "settings.manage"
if path.startswith("/api/apps/") and path.endswith(("/metadata", "/container-mapping")):
return "inventory.manage"