Add confirmed admin override for provisioning player checks

This commit is contained in:
2026-10-05 18:16:02 -04:00
parent 4b0f341d64
commit 8abe71b915
20 changed files with 362 additions and 26 deletions
+12 -7
View File
@@ -32,7 +32,7 @@ import urllib.parse
import urllib.request
import uuid
VERSION = "1.4.14"
VERSION = "1.4.15"
LABEL = "io.electric-cherry"
MAX_FILE = 32 * 1024 * 1024
MANIFEST_MAX_ENTRIES = 10000
@@ -1332,7 +1332,7 @@ class Agent:
if line.startswith("PRETTY_NAME="):
os_name = line.split("=", 1)[1].strip('"')
update = self.update_inventory()
capabilities = ["library.remove", "instance.cancel-create", "operations.read-lane", "operations.health-lane", "rcon.status-batch", "instance.heartbeat-state", "files.manifest"] + (["host.decommission"] if self.can_uninstall() else []) + (["agent.update"] if update["enabled"] else [])
capabilities = ["library.remove", "instance.cancel-create", "instance.safety-override", "operations.read-lane", "operations.health-lane", "rcon.status-batch", "instance.heartbeat-state", "files.manifest"] + (["host.decommission"] if self.can_uninstall() else []) + (["agent.update"] if update["enabled"] else [])
return {"hostname": socket.gethostname(), "agentVersion": VERSION, "protocolVersion": 1, "capabilities": capabilities, "agentUpdate": update, "os": os_name, "osVersion": platform.release(), "architecture": platform.machine(), "cpu": platform.processor() or platform.machine(), "cpuCount": os.cpu_count() or 0, **resources, **({"cpuUsagePercent": cpu_usage} if cpu_usage is not None else {}), "memoryTotal": memory.get("MemTotal", 0), "memoryAvailable": memory.get("MemAvailable", 0), "storageTotal": usage.total, "storageAvailable": usage.free, "dockerVersion": docker_version, "dockerRunning": docker_running, "directoriesWritable": os.access(self.root, os.W_OK), "runtimeImages": sorted(set(images)), "libraries": libraries, "libraryCaches": caches, "currentOperation": self.current_operation, "portRange": self.config.get("portRange", {"min": 28960, "max": 29060})}
def safe_to_disrupt(self, instance_id, generation):
@@ -1569,16 +1569,20 @@ class Agent:
else:
self.docker.call("container", "start", name)
elif kind in ("instance.stop", "instance.restart", "instance.remove", "instance.rebuild"):
# The authenticated controller records explicit administrator approval
# for this immutable operation. Ownership and generation checks still apply.
override_safety = payload.get("safetyOverride") is True
if kind == "instance.rebuild":
next_generation = payload.get("nextGeneration", payload.get("newGeneration"))
if isinstance(next_generation, bool) or not isinstance(next_generation, int) or next_generation <= generation:
raise AgentError("Rebuild must use a newer immutable generation")
spec = payload.get("spec") or {key: value for key, value in payload.items() if key not in ("nextGeneration", "newGeneration")}
self.validate_spec(spec)
self.safe_to_disrupt(instance_id, generation)
if not override_safety:
self.safe_to_disrupt(instance_id, generation)
self.checkpoint({"phase": "disrupting", "containerId": health.get("containerId"), "startedAt": health.get("startedAt")})
if payload.get("stageId") and kind == "instance.restart":
before = self.activate(instance_id, generation, payload["stageId"])
before = self.activate(instance_id, generation, payload["stageId"], override_safety=override_safety)
if kind in ("instance.remove", "instance.rebuild"):
if health["exists"]:
self.docker.call("container", "stop", "--time", "30", name)
@@ -1592,7 +1596,7 @@ class Agent:
shutil.rmtree(directory)
self.mark_removed(instance_id, generation)
elif health["exists"]:
if kind == "instance.restart" and payload.get("stageId"):
if kind == "instance.restart" and payload.get("stageId") and not override_safety:
self.safe_to_disrupt(instance_id, generation)
self.docker.call("container", "stop" if kind.endswith("stop") else "restart", "--time", "30", name)
else:
@@ -1663,7 +1667,7 @@ class Agent:
atomic_json(marker, manifest)
return {"stageId": stage_id, "files": len(planned), "state": "staged"}
def activate(self, instance_id, generation, stage_id):
def activate(self, instance_id, generation, stage_id, *, override_safety=False):
directory = self.instance_dir(instance_id, generation)
stage = directory / "stages" / identifier(stage_id)
marker = stage / "manifest.json"
@@ -1681,7 +1685,8 @@ class Agent:
before.append({"id": item.get("id"), "path": item["path"], "contentBase64": None if content is None else base64.b64encode(content).decode()})
# Snapshot preparation may take time. Occupancy is rechecked at the
# local activation boundary, after all file I/O and integrity checks.
self.safe_to_disrupt(instance_id, generation)
if not override_safety:
self.safe_to_disrupt(instance_id, generation)
saved.update(state="activating", before=before)
atomic_json(marker, saved)
try:
+2 -2
View File
@@ -1,8 +1,8 @@
{
"version": "1.4.14",
"version": "1.4.15",
"protocolVersion": 1,
"platform": "debian-amd64",
"sha256": "3e2e11389f412a0f1b2746c85b9afa626c5e0fcaa4801bd13f1de283abf8830e",
"sha256": "ddd1f7d557e7d4441b7cff1d8cbc4ed80aed7e78a6f1ab29656c5bdb55410543",
"updaterProtocolVersion": 1,
"updaterSha256": "066bb896726600ece9ce72697ce523c9a707cc9a621505931ba0892dec59d3c8"
}