#!/usr/bin/env python3 """Real MicroVM/Nginx Deploy acceptance on an empty, disposable Linux CI host. Uses locally built OCI images and a private, fake Auth fixture. The optional public Tunnel mode contacts the production connection service and requires separate target approval and two disposable dashboard-issued tokens. """ import argparse import concurrent.futures import contextlib import gzip import hashlib import http.client import http.server import io import json import os from pathlib import Path import platform import socket import sqlite3 import subprocess import sys import tarfile import threading import time from urllib.parse import urlsplit import uuid from deploy_fleet_qa import host_evidence, run_fleet from deploy_rotation_qa import run_rotation from deploy_auto_qa import run_auto from deploy_recovery_qa import run_recovery from deploy_queue_qa import run_queue from deploy_ci_qa import run_ci from deploy_observation_qa import run_observation from public_tunnel_credentials import load_credentials, verify_registration from public_tunnel_fixture import image_fixture as public_image_fixture, static_elf_arch from public_tunnel_setup import run_fixture_services from public_tunnel_acceptance import run_public def digest(data): return "sha256:" + hashlib.sha256(data).hexdigest() def image_fixture(busybox, label, healthy=True): layer = io.BytesIO() with tarfile.open(fileobj=layer, mode="w") as archive: for directory in ["bin", "www", "www/cgi-bin"]: entry = tarfile.TarInfo(directory) entry.type = tarfile.DIRTYPE entry.mode = 0o755 archive.addfile(entry) files = {"bin/busybox": busybox, "www/index.html": f"release-{label}\n".encode(), "bin/observed-init": b'#!/bin/sh\nprintf "fixture-stdout-%s\\n" "$OBS_ID"\nprintf "fixture-stderr-%s\\n" "$OBS_ID" >&2\nexec /bin/busybox httpd -f -p 8080 -h /www\n'} if healthy: files["www/health"] = b"healthy\n" for path, data in files.items(): entry = tarfile.TarInfo(path) entry.mode = 0o755 if path.startswith("bin/") else 0o644 entry.size = len(data) archive.addfile(entry, io.BytesIO(data)) shell = tarfile.TarInfo("bin/sh") shell.type = tarfile.SYMTYPE shell.linkname = "busybox" shell.mode = 0o777 archive.addfile(shell) scripts = { "hold": f"""#!/bin/sh /bin/busybox rm -f /tmp/deploy-release /bin/busybox touch /tmp/deploy-holding printf 'Content-Type: text/plain\\r\\n\\r\\n' while [ ! -f /tmp/deploy-release ]; do printf ' '; /bin/busybox sleep 1; done printf 'release-{label}\\n' """, "release": "#!/bin/sh\n/bin/busybox touch /tmp/deploy-release\nprintf 'Content-Type: text/plain\\r\\n\\r\\nreleased\\n'\n", "holding": "#!/bin/sh\nprintf 'Content-Type: text/plain\\r\\n\\r\\n'\nif [ -f /tmp/deploy-holding ]; then echo holding; else echo idle; fi\n", "environment": "#!/bin/sh\nprintf 'Content-Type: text/plain\\r\\n\\r\\n%s:%s\\n' \"$FLEET_SERVICE\" \"$FLEET_RELEASE\"\n", "rotation": """#!/bin/sh case "$PASSWORD" in qa-secret-v0) reply=old ;; qa-secret-v1) reply=next ;; qa-secret-v2) reply=latest ;; *) if [ "$UNRELATED" = 1 ]; then reply=unrelated; else reply=invalid; fi ;; esac printf 'Content-Type: text/plain\\r\\nContent-Length: %s\\r\\n\\r\\n%s\\n' "$((${#reply} + 1))" "$reply" """, "rotation-health": """#!/bin/sh case "$PASSWORD:$UNRELATED" in qa-secret-v0:*|qa-secret-v1:*|qa-secret-v2:*|:1) printf 'Content-Type: text/plain\\r\\n\\r\\nhealthy\\n' ;; *) printf 'Status: 503 Service Unavailable\\r\\nContent-Type: text/plain\\r\\n\\r\\nunhealthy\\n' ;; esac """, "recovery-health": "#!/bin/sh\nif [ -f /unhealthy ]; then printf 'Status: 503 Service Unavailable\\r\\n'; fi\nprintf 'Content-Type: text/plain\\r\\n\\r\\nhealth\\n'\n", "fail-health": "#!/bin/sh\n/bin/busybox touch /unhealthy\nprintf 'Content-Type: text/plain\\r\\n\\r\\ninjected\\n'\n", "observation": """#!/bin/sh printf 'fixture-cgi-%s\\n' "$OBS_ID" >&2 printf 'Content-Type: text/plain\\r\\n\\r\\nobserved\\n' """, "otel-environment": """#!/bin/sh printf 'Content-Type: text/plain\\r\\n\\r\\n%s|%s|%s\\n' "$OTEL_EXPORTER_OTLP_ENDPOINT" "$OTEL_EXPORTER_OTLP_PROTOCOL" "$OTEL_SERVICE_NAME" """, "internal-call": """#!/bin/sh reply=$(/bin/busybox wget -q -O - "http://$SELF_INTERNAL/cgi-bin/rotation") || { printf 'Status: 502 Bad Gateway\\r\\nContent-Type: text/plain\\r\\n\\r\\nunavailable\\n' exit 0 } printf 'Content-Type: text/plain\\r\\n\\r\\n%s\\n' "$reply" """, } for name, script in scripts.items(): data = script.encode() entry = tarfile.TarInfo("www/cgi-bin/" + name) entry.mode = 0o755 entry.size = len(data) archive.addfile(entry, io.BytesIO(data)) raw = layer.getvalue() compressed = gzip.compress(raw, mtime=0) architecture = "arm64" if platform.machine() in ["aarch64", "arm64"] else "amd64" config = json.dumps({"architecture": architecture, "os": "linux", "rootfs": {"type": "layers", "diff_ids": [digest(raw)]}, "config": {"Env": ["PATH=/bin"], "Entrypoint": ["/bin/sh", "/bin/observed-init"]}}, separators=(",", ":")).encode() manifest = json.dumps({"schemaVersion": 2, "mediaType": "application/vnd.oci.image.manifest.v1+json", "config": {"mediaType": "application/vnd.oci.image.config.v1+json", "digest": digest(config), "size": len(config)}, "layers": [{"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", "digest": digest(compressed), "size": len(compressed)}]}, separators=(",", ":")).encode() return digest(manifest), manifest, {digest(config): config, digest(compressed): compressed} class Registry: def __init__(self, fixtures): self.fixtures = fixtures manifests = {item[0]: item[1] for item in fixtures} blobs = {key: value for item in fixtures for key, value in item[2].items()} class Handler(http.server.BaseHTTPRequestHandler): def do_GET(self): key = self.path.rsplit("/", 1)[-1] body = b"{}" if self.path in ["/v2/", "/v2"] else manifests.get(key, blobs.get(key)) self.send_response(200 if body is not None else 404) body = body if body is not None else b"missing" self.send_header("Content-Length", str(len(body))) self.send_header("Docker-Content-Digest", digest(body)) self.send_header("Content-Type", "application/vnd.oci.image.manifest.v1+json" if key in manifests else "application/octet-stream") self.end_headers() self.wfile.write(body) def log_message(self, *args): pass self.server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler) self.thread = threading.Thread(target=self.server.serve_forever, daemon=True) self.thread.start() @property def address(self): return f"127.0.0.1:{self.server.server_port}" def close(self): self.server.shutdown() self.server.server_close() self.thread.join() def wait_for(check, timeout=180, message="condition timed out"): deadline = time.monotonic() + timeout while time.monotonic() < deadline: result = check() if result: return result time.sleep(.1) raise AssertionError(message) _UNSET = object() class Api: def __init__(self, base): address = urlsplit(base) assert address.scheme == "http" and address.hostname == "127.0.0.1", "local disposable API required" self.port = address.port or 80 def call(self, method, path, body=_UNSET, allowed=(200, 201, 202, 204)): with contextlib.closing(http.client.HTTPConnection("127.0.0.1", self.port, timeout=30)) as connection: connection.request(method, path, body=json.dumps(body) if body is not _UNSET else None, headers={"Content-Type": "application/json"}) response = connection.getresponse() raw = response.read() assert response.status in allowed, f"{method} {path}: {response.status} {raw[:1000]!r}" return json.loads(raw) if raw else None def ready(self): def check(): try: return self.call("GET", "/api/host") except (OSError, AssertionError, http.client.HTTPException): return None return wait_for(check, message="API did not become ready") def traffic(host, port, domain, path="/"): with contextlib.closing(http.client.HTTPConnection(host, port, timeout=120 if path == "/cgi-bin/hold" else 5)) as connection: stage = "request" try: connection.request("GET", path, headers={"Host": domain}) stage = "response headers" response = connection.getresponse() stage = "response body" return response.status, response.read().decode().strip() except TimeoutError as error: raise TimeoutError(f"{stage} timed out") from error def atomic_json(path, value): metadata = path.stat() temporary = path.with_suffix(".deploy-qa-tmp") temporary.write_text(json.dumps(value)) os.chown(temporary, metadata.st_uid, metadata.st_gid) temporary.chmod(metadata.st_mode & 0o777) temporary.replace(path) def require_empty_host(api): for resource in ["vms", "micro-services", "pools", "pipe/auth"]: assert not api.call("GET", "/api/" + resource), f"refusing existing {resource}" unit = "firecrab-ingress.service" assert subprocess.check_output(["systemctl", "show", "--property=LoadState", "--value", unit], text=True).strip() == "not-found", "refusing existing ingress" assert not api.call("GET", "/api/pipe").get("config"), "refusing existing Pipe configuration" def run(args, evidence): assert args.disposable_host and os.geteuid() == 0 and Path("/dev/kvm").exists() required_cpus = 4 if args.secret_rotation or args.replica_recovery or args.queue_controls or args.automatic_deploy or args.ci_deploy or args.observability or args.public_tunnel else 3 assert int(subprocess.check_output(["nproc"], text=True)) >= required_cpus, f"{required_cpus} CPUs required for the selected rolling-surge acceptance" if args.fifteen_services: evidence["host"] = host_evidence(args.images) assert "httpd" in subprocess.check_output([args.busybox, "--list"], text=True).splitlines() api = Api(args.api) api.ready() require_empty_host(api) credentials = load_credentials(args.public_token) if args.public_tunnel else None if args.public_tunnel: evidence["publicTarget"] = {"hostname": platform.node(), "architecture": platform.machine(), "addresses": [credential.label for credential in credentials]} evidence["publicRegistration"] = [verify_registration(credential) for credential in credentials] if args.secret_rotation or args.replica_recovery: assert not api.call("GET", "/api/secrets"), "refusing existing Secrets" assert not api.call("GET", "/api/secrets/connections"), "refusing existing Secret connections" unit = "firecrab-ingress.service" fixtures = [image_fixture(Path(args.busybox).read_bytes(), label, label != "bad") for label in ["A", "B", "bad", "C"]] public_fixtures = [] if args.public_tunnel: binary = args.public_fixture_binary.read_bytes() expected_arch = {"aarch64": "arm64", "arm64": "arm64", "x86_64": "amd64", "amd64": "amd64"}.get(platform.machine()) if expected_arch is None or static_elf_arch(binary) != expected_arch: raise ValueError("fixture architecture must match the host") public_fixtures = [public_image_fixture(binary, label) for label in ["A", "B"]] all_fixtures = fixtures + public_fixtures registry = Registry(all_fixtures) images = Path(args.images) registrations = images / ".templates.json" database = Path(args.database) assert database.is_file(), "explicit installed API database required" aliases = [] rootfs_b = None saved_rootfs = None network = service = None ingress_created = auth_seeded = False stop = threading.Event() pause = threading.Event() samples, failures = [], [] loader = None held = [] held_ips = [] executor = concurrent.futures.ThreadPoolExecutor(max_workers=12) fragment = None with socket.socket() as reserved: reserved.bind(("127.0.0.1", 0)) ingress_port = reserved.getsockname()[1] domain = f"deploy-{uuid.uuid4().hex[:8]}.example.test" def service_path(suffix=""): return f"/api/micro-services/{service}" + suffix def operation(identifier): return api.call("GET", service_path(f"/deployments/{identifier}")) def terminal(identifier): def check(): current = operation(identifier) return current if current["phase"] in ["running", "failed", "stopped"] else None return wait_for(check, message=f"Deploy {identifier} did not finish") def deploy(spec, previous): request = {"operationId": str(uuid.uuid4()), "expectedReleaseId": previous["id"], "spec": spec, "strategy": "rolling"} return api.call("POST", service_path("/updates"), request) def begin_hold(current): nonlocal held_ips held_ips = [instance["ipv4"] for instance in current["instances"]] pause.set() try: time.sleep(.1) # Pause only fixture setup so round-robin assigns one held request # per old replica. Continuous traffic resumes before Deploy starts. held.extend(executor.submit(traffic, "127.0.0.1", ingress_port, domain, "/cgi-bin/hold") for _ in held_ips) for address in held_ips: wait_for(lambda: traffic(address, 8080, domain, "/cgi-bin/holding") == (200, "holding"), timeout=10) finally: pause.clear() def release_holds(): for address in held_ips: traffic(address, 8080, domain, "/cgi-bin/release") for future in held: assert future.result(timeout=10) == (200, "release-A") held.clear() held_ips.clear() def load(): while not stop.is_set(): if pause.is_set(): stop.wait(.01) continue try: value = traffic("127.0.0.1", ingress_port, domain) samples.append(value) if value not in [(200, "release-A"), (200, "release-B")]: failures.append(value) except Exception as error: failures.append(str(error)) time.sleep(.02) def stop_service(): wait_for(lambda: api.call("GET", service_path("/deployment"))["phase"] in ["running", "failed", "degraded", "stopped"]) api.call("POST", service_path("/stop")) wait_for(lambda: api.call("GET", service_path("/deployment"))["phase"] == "stopped", timeout=180) api.call("DELETE", service_path()) try: for digest_value, _, _ in all_fixtures: result = api.call("POST", "/api/oci/import", {"reference": f"{registry.address}/team/deploy-qa@{digest_value}"}) alias = result["alias"] aliases.append(alias) result = wait_for(lambda: (value if (value := api.call("GET", f"/api/oci/import/{alias}"))["status"] in ["succeeded", "failed"] else None), timeout=300) assert result["status"] == "succeeded", result subprocess.run(["systemctl", "stop", "firecrab-api.service"], check=True) try: entries = json.loads(registrations.read_text()) for source_alias, (digest_value, _, _) in zip(aliases.copy(), all_fixtures): spec = next(entry.copy() for entry in entries if entry["alias"] == source_alias) spec["alias"] = "ghcr.io-team-deploy-qa-sha256-" + digest_value.split(":")[1][:12] assert spec["version"] == digest_value assert not any(entry["alias"] == spec["alias"] for entry in entries), "refusing an existing fixture alias" if digest_value == fixtures[1][0]: rootfs_b = images / spec["rootfs"] entries.append(spec) aliases.append(spec["alias"]) atomic_json(registrations, entries) with sqlite3.connect(database) as connection: assert connection.execute("SELECT count(*) FROM pipe_auth_credentials").fetchone()[0] == 0 connection.execute("INSERT INTO pipe_auth_credentials(provider, credential) VALUES('github',?)", (json.dumps({"provider": "github", "username": "team", "token": "local-deploy-qa-only"}),)) auth_seeded = True finally: subprocess.run(["systemctl", "start", "firecrab-api.service"], check=True) api.ready() ingress_created = True api.call("PUT", "/api/pipe", {"config": {"listen": f"127.0.0.1:{ingress_port}", "routes": [], "tunnel": None}, "credentials": None}) fragment = subprocess.check_output(["systemctl", "show", "--property=FragmentPath", "--value", unit], text=True).strip() network = api.call("POST", "/api/micro-networks", {"name": "deploy-qa-" + uuid.uuid4().hex[:8], "subnetCidr": "172.29.87.0/24", "internetEnabled": True})["id"] spec_a = {"name": "deploy-qa-" + uuid.uuid4().hex[:8], "image": "ghcr.io/team/deploy-qa@" + fixtures[0][0], "port": 8080, "healthPath": "/health", "domain": domain, "ram": 128, "cpu": 1, "replicas": 1, "startupTimeout": 10, "drainTimeout": 8, "env": {}, "secrets": {}} service = api.call("POST", "/api/micro-services", spec_a)["id"] initial = api.call("POST", service_path("/deployment"), {"microNetworkId": network}) initial = terminal(initial["id"]) assert initial["phase"] == "running", initial assert traffic("127.0.0.1", ingress_port, domain) == (200, "release-A") wait_for(lambda: not any(worker["draining"] for worker in api.call("GET", "/api/pipe")["ingressWorkers"]), timeout=10) loader = threading.Thread(target=load, daemon=True) loader.start() begin_hold(initial) spec_b = {**spec_a, "image": "ghcr.io/team/deploy-qa@" + fixtures[1][0]} updating = deploy(spec_b, initial) wait_for(lambda: operation(updating["id"])["phase"] == "draining", timeout=180) wait_for(lambda: traffic("127.0.0.1", ingress_port, domain) == (200, "release-B"), timeout=10) assert not any(future.done() for future in held) ingress_pid = api.call("GET", "/api/pipe")["units"][0]["mainPid"] subprocess.run(["systemctl", "restart", "firecrab-api.service"], check=True) api.ready() assert api.call("GET", "/api/pipe")["units"][0]["mainPid"] == ingress_pid failed_drain = terminal(updating["id"]) assert failed_drain["phase"] == "failed" and "drainTimeout" in failed_drain["error"], failed_drain for instance in initial["instances"]: assert api.call("GET", "/api/vms/" + instance["vmId"])["state"] == "running" release_holds() api.call("POST", service_path("/deployment/retry"), {"operationId": updating["id"]}) active = terminal(updating["id"]) assert active["phase"] == "running", active print("PASS real single-replica update, retained HTTP request, API restart, drain timeout and retry", flush=True) rollback_request = {"operationId": str(uuid.uuid4()), "expectedReleaseId": active["id"], "releaseId": initial["id"]} restored = api.call("POST", service_path("/rollback"), rollback_request) active = terminal(restored["id"]) assert active["phase"] == "running" and active["digest"] == fixtures[0][0], active assert api.call("POST", service_path("/rollback"), rollback_request)["id"] == active["id"] assert traffic("127.0.0.1", ingress_port, domain) == (200, "release-A") print("PASS selected digest/configuration rollback and duplicate request identity", flush=True) spec_two = {**spec_a, "replicas": 2} scaling = deploy(spec_two, active) active = terminal(scaling["id"]) assert active["phase"] == "running" and len(active["instances"]) == 2, active begin_hold(active) partial = deploy({**spec_b, "replicas": 2, "drainTimeout": 30}, active) wait_for(lambda: operation(partial["id"])["phase"] == "draining", timeout=180) # The first replacement is admitted. Keep its old workers draining while # damaging only this fixture's source cache, then force replacement two to fail. saved_rootfs = rootfs_b.with_suffix(".deploy-qa-saved") assert not saved_rootfs.exists() rootfs_b.rename(saved_rootfs) rootfs_b.write_bytes(b"intentional-invalid-qa-rootfs") release_holds() failed = terminal(partial["id"]) assert failed["phase"] == "failed" and failed["update"]["completed"], failed current = api.call("GET", service_path("/deployment")) assert current["id"] == active["id"] and current["phase"] == "running" and len(current["instances"]) == 2, current for instance in current["instances"]: assert api.call("GET", "/api/vms/" + instance["vmId"])["templateVersion"] == fixtures[0][0] saved_rootfs.replace(rootfs_b) saved_rootfs = None print("PASS partial rolling failure recreates the full previous two-replica release", flush=True) bad = deploy({**spec_two, "image": "ghcr.io/team/deploy-qa@" + fixtures[2][0], "startupTimeout": 2}, current) failed = terminal(bad["id"]) assert failed["phase"] == "failed" and failed["update"]["completed"], failed current = api.call("GET", service_path("/deployment")) assert current["id"] == active["id"] and current["phase"] == "running" and len(current["instances"]) == 2, current assert len(samples) > 20 and not failures, {"samples": len(samples), "failures": failures[:10]} print(f"PASS unhealthy candidate stays out of traffic; {len(samples)} continuous HTTP requests, zero errors", flush=True) evidence["coreRequests"] = len(samples) if args.fifteen_services or args.secret_rotation or args.replica_recovery or args.queue_controls or args.automatic_deploy or args.ci_deploy or args.observability or args.public_tunnel: stop.set() loader.join(timeout=10) stop_service() service = None if args.secret_rotation: run_rotation(api, wait_for, traffic, network, spec_a, ingress_port, evidence) elif args.fifteen_services: run_fleet(api, wait_for, traffic, network, spec_a, fixtures[1][0], ingress_port, evidence) if args.replica_recovery: run_recovery(api, wait_for, traffic, network, spec_a, ingress_port, evidence) if args.queue_controls: run_queue(api, wait_for, traffic, network, spec_a, fixtures[1][0], ingress_port, evidence) if args.automatic_deploy: run_auto(api, wait_for, traffic, network, spec_a, fixtures, ingress_port, evidence) if args.ci_deploy: run_ci(api, wait_for, traffic, network, spec_a, fixtures, ingress_port, evidence) if args.observability: run_observation(api, wait_for, traffic, network, spec_a, ingress_port, evidence) if args.public_tunnel: run_fixture_services(api, wait_for, network, spec_a, public_fixtures, credentials, evidence, run_public) finally: original_error = sys.exc_info()[0] is not None stop.set() if loader: loader.join(timeout=10) for address in held_ips: with contextlib.suppress(Exception): traffic(address, 8080, domain, "/cgi-bin/release") executor.shutdown(wait=True, cancel_futures=True) if saved_rootfs and saved_rootfs.exists(): saved_rootfs.replace(rootfs_b) cleanup_errors = [] def cleanup(label, action): try: action() except Exception as error: cleanup_errors.append(f"{label}: {error}") cleanup("API readiness after fixture restart", api.ready) if service: cleanup("service VMs and configuration", stop_service) if network: cleanup("network", lambda: api.call("DELETE", "/api/micro-networks/" + network)) for alias in reversed(aliases): cleanup("fixture image " + alias, lambda alias=alias: api.call("DELETE", "/api/images/" + alias, allowed=(204, 404))) if auth_seeded: cleanup("fixture Auth", lambda: api.call("DELETE", "/api/pipe/auth/github")) if ingress_created: cleanup("ingress disable", lambda: api.call("POST", "/api/pipe/disable")) cleanup("ingress unit", lambda: subprocess.run(["systemctl", "disable", "--now", unit], check=True)) if fragment: cleanup("ingress fragment", lambda: Path(fragment).unlink(missing_ok=True)) cleanup("unit reload", lambda: subprocess.run(["systemctl", "daemon-reload"], check=True)) for name in ["manifest.json", "ingress.conf"]: cleanup("Pipe file " + name, lambda name=name: (Path(args.config) / "pipe" / name).unlink(missing_ok=True)) registry.close() evidence["cleanupErrors"] = cleanup_errors if cleanup_errors and not original_error: raise RuntimeError("Deploy QA cleanup failed; see report") assert not api.call("GET", "/api/vms") and not api.call("GET", "/api/micro-services") if args.public_tunnel: evidence["deployFixtureResourcesRemoved"] = True evidence["allResourcesRemoved"] = False print("PASS Deploy QA fixtures removed; public reservations and token files require operator cleanup", flush=True) else: evidence["allResourcesRemoved"] = True print("PASS all dedicated Deploy QA VMs, services, routes, network, fixture images and credentials removed", flush=True) if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--disposable-host", action="store_true", required=True) parser.add_argument("--api", default="http://127.0.0.1:5523") parser.add_argument("--database", default="/var/lib/firecrab/data/firecrab.db") parser.add_argument("--images", default="/var/lib/firecrab/images") parser.add_argument("--config", default="/etc/firecrab") parser.add_argument("--busybox", default="/usr/bin/busybox") modes = parser.add_mutually_exclusive_group() modes.add_argument("--fifteen-services", action="store_true", help="require 16 CPUs and run the live 15-service acceptance") modes.add_argument("--secret-rotation", action="store_true", help="require four CPUs and run live shared-Secret rotation acceptance") parser.add_argument("--replica-recovery", action="store_true", help="run live replica restart/replacement and API recovery acceptance") parser.add_argument("--automatic-deploy", action="store_true", help="run authenticated tag monitoring and rollback/restart acceptance with private TLS trust") parser.add_argument("--queue-controls", action="store_true", help="run live queued cancellation, rollback priority and API restart acceptance") parser.add_argument("--ci-deploy", action="store_true", help="run scoped CLI/Action acceptance through private verified TLS and Nginx") parser.add_argument("--observability", action="store_true", help="run isolated app logs, API restart, events and OTLP acceptance") parser.add_argument("--public-tunnel", action="store_true", help="run explicitly approved public Tunnel acceptance") parser.add_argument("--public-target-approved", action="store_true", help="confirm separate approval for the host and public test addresses") parser.add_argument("--public-token", nargs=2, action="append", metavar=("LABEL", "PRIVATE_FILE"), help="owned qa- label and private token file; repeat twice") parser.add_argument("--public-fixture-binary", type=Path, help="statically linked Linux public Tunnel fixture") parser.add_argument("--report", type=Path, help="write measured evidence even when acceptance fails") args = parser.parse_args() if (args.fifteen_services or args.secret_rotation or args.replica_recovery or args.queue_controls or args.automatic_deploy or args.ci_deploy or args.observability or args.public_tunnel) and not args.report: parser.error("extended acceptance requires --report") if args.public_tunnel: if not args.public_target_approved or not args.public_fixture_binary or not args.public_token or len(args.public_token) != 2: parser.error("public Tunnel acceptance requires explicit target approval, a fixture binary, and exactly two token files") if args.api != "http://127.0.0.1:5523" or args.config != "/etc/firecrab" or args.database != "/var/lib/firecrab/data/firecrab.db" or args.images != "/var/lib/firecrab/images": parser.error("public Tunnel acceptance requires the default disposable host paths and API") if args.fifteen_services or args.secret_rotation or args.replica_recovery or args.queue_controls or args.automatic_deploy or args.ci_deploy or args.observability: parser.error("public Tunnel acceptance cannot be combined with other extended modes") elif args.public_target_approved or args.public_fixture_binary or args.public_token: parser.error("public Tunnel options require --public-tunnel") evidence = {"status": "failed", "sourceCommit": subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip()} try: run(args, evidence) evidence["status"] = "pending_operator_verification" if args.public_tunnel else "passed" except BaseException as error: evidence["error"] = str(error) raise finally: if args.report: args.report.parent.mkdir(parents=True, exist_ok=True) args.report.write_text(json.dumps(evidence, indent=2) + "\n")