From 4f1419ca93e834e0d2a699403a3bc4eef31b7994 Mon Sep 17 00:00:00 2001 From: Jino Jose Date: Mon, 17 Aug 2026 11:05:19 +0530 Subject: [PATCH] release: freeze Nexus One AI 1.0.0-rc1 --- ansible/roles/cezen-backend/files/main.py | 323 +++++++- .../roles/cezen-backend/files/train_qlora.py | 18 +- autoinstall/offline/README.md | 90 +- .../offline/activation-health-window.py | 247 ++++++ autoinstall/offline/atomic-profile-journal.py | 138 ++++ autoinstall/offline/build-bundle.sh | 12 +- autoinstall/offline/container-sources.json | 8 + autoinstall/offline/install-python-profile.py | 188 +++++ .../offline/payload-lock.candidate.json | 26 + autoinstall/offline/payload.env | 4 +- .../offline/plan-python-runtime-profile.py | 106 +++ .../prepare-python-profile-operation.py | 91 ++ .../offline/python-environment-checkpoint.py | 215 +++++ autoinstall/offline/python-packages.txt | 4 + .../offline/python-runtime-profiles.json | 29 + .../offline/recover-python-profile-on-boot.py | 58 ++ autoinstall/offline/resolve-apt-lock.py | 106 +++ autoinstall/offline/resolve-payload-lock.py | 138 ++++ autoinstall/offline/resolve-python-lock.py | 148 ++++ autoinstall/offline/validate-payload-lock.py | 118 +++ .../validate-python-runtime-profile.py | 60 ++ .../verify-python-profile-membership.py | 111 +++ cezen-portal/about.html | 17 +- cezen-portal/admin.html | 17 +- cezen-portal/agents.html | 17 +- cezen-portal/analytics.html | 17 +- cezen-portal/api-playground.html | 20 +- cezen-portal/apikeys.html | 21 +- cezen-portal/app-studio.html | 41 + cezen-portal/appliance.html | 17 +- cezen-portal/applications.html | 15 + cezen-portal/audit.html | 16 +- cezen-portal/auth.js | 89 ++ cezen-portal/benchmark.html | 33 +- cezen-portal/branding.js | 15 +- cezen-portal/change-password.html | 2 +- cezen-portal/chat-multi.html | 17 +- cezen-portal/chatrooms.html | 17 +- cezen-portal/connectors.html | 17 +- cezen-portal/console.html | 17 +- cezen-portal/dashboard.html | 17 +- cezen-portal/dataset-creation.html | 389 +++++++++ cezen-portal/documents.html | 17 +- cezen-portal/evals.html | 17 +- cezen-portal/faq.html | 17 +- cezen-portal/feedback.html | 16 +- cezen-portal/glossary.html | 17 +- cezen-portal/guardrails.html | 17 +- cezen-portal/health.html | 17 +- cezen-portal/index.html | 107 +-- cezen-portal/knowledge.html | 17 +- cezen-portal/login.html | 2 +- cezen-portal/meeting.html | 17 +- cezen-portal/model-compare.html | 88 +- cezen-portal/models-admin.html | 17 +- cezen-portal/models.html | 17 +- cezen-portal/nexus-compare.html | 43 + cezen-portal/nexus-notebook.html | 287 +++++++ cezen-portal/notifications.html | 16 +- cezen-portal/prompt-studio.html | 496 ++++++++--- cezen-portal/prompts.html | 17 +- cezen-portal/quickstart.html | 19 +- cezen-portal/rag-quality.html | 17 +- cezen-portal/router.html | 17 +- cezen-portal/schedules.html | 17 +- cezen-portal/security.html | 14 +- cezen-portal/settings.html | 17 +- cezen-portal/teams.html | 16 +- cezen-portal/tool-chromadb.html | 17 +- cezen-portal/tool-dcgm.html | 17 +- cezen-portal/tool-fastapi.html | 17 +- cezen-portal/tool-jupyter.html | 17 +- cezen-portal/tool-langchain.html | 17 +- cezen-portal/tool-nvtop.html | 17 +- cezen-portal/tool-ollama.html | 17 +- cezen-portal/tool-openwebui.html | 17 +- cezen-portal/tool-qlora.html | 17 +- cezen-portal/training.html | 126 ++- cezen-portal/troubleshooting.html | 17 +- cezen-portal/usecases.html | 17 +- cezen-portal/users.html | 17 +- cezen-portal/whats-new.html | 17 +- cezen-portal/workflows.html | 17 +- ...I_Product_Presentation.pptx.inspect.ndjson | 209 +++++ how-it-flows.gif | Bin 0 -> 7868074 bytes release/1.0.0-rc1/README.md | 15 + release/1.0.0-rc1/agent-registry.schema.json | 25 + release/1.0.0-rc1/build-rc.sh | 20 + release/1.0.0-rc1/feature-profiles.json | 21 + release/1.0.0-rc1/make-trial.py | 39 + release/1.0.0-rc1/qualify.py | 34 + release/1.0.0-rc1/release.json | 20 + .../analysis/gate-1-decision-dossier.md | 119 +++ .../analysis/gate-evidence-allocation.md | 67 ++ .../analysis/installer-release-consistency.md | 349 ++++++++ .../checklists/requirements.md | 30 + .../contracts/architecture-decisions.md | 62 ++ .../contracts/compatibility-evaluation.md | 62 ++ .../contracts/installer-artifact-contract.md | 138 ++++ .../contracts/installer-threat-model.md | 125 +++ .../contracts/model-pack-lifecycle.md | 118 +++ .../contracts/offline-license-policy.md | 164 ++++ .../contracts/offline-validation-lab.md | 165 ++++ .../contracts/online-universal-acquisition.md | 110 +++ .../contracts/payload-inventory-sizing.md | 134 +++ .../python-runtime-profile-selection.md | 172 ++++ .../contracts/release-lifecycle-provenance.md | 167 ++++ .../contracts/release-signing-trust.md | 144 ++++ .../schemas/artifact-manifest.schema.json | 180 ++++ ...embership-verification-receipt.schema.json | 30 + .../schemas/signature-envelope.schema.json | 20 + .../contracts/systemd/README.md | 74 ++ .../contracts/systemd/cezen-api.service | 33 + .../contracts/systemd/chromadb.service | 27 + .../nexus-one-activation-health.service | 19 + .../nexus-one-activation-rollback.service | 16 + .../systemd/nginx-cezen-ordering.conf | 5 + .../contracts/test-vectors/README.md | 26 + .../test-vectors/compatibility-cases.json | 30 + .../test-vectors/ed25519-test-public.pem | 3 + .../model-pack-manifest.valid.json | 11 + .../model-pack-signature.valid.json | 9 + .../python-profile-journal-resume.json | 5 + ...le-membership-receipt.structural-test.json | 15 + .../python-profile-probe-gpu-lite.json | 11 + .../test-vectors/signature-cases.json | 29 + .../trust-bundle-manifest.valid.json | 10 + .../trust-bundle-signature.valid.json | 9 + .../update-bundle-manifest.valid.json | 11 + .../update-bundle-signature.valid.json | 9 + .../test-vectors/validate_vectors.py | 162 ++++ .../test-vectors/verification-evidence.md | 18 + .../workstation-iso-manifest.valid.json | 55 ++ .../workstation-iso-signature.valid.json | 9 + .../contracts/update-rollback-policy.md | 159 ++++ .../workstation-hardware-model-baseline.md | 108 +++ ...kstation-runtime-compatibility-baseline.md | 97 +++ .../evidence/gate2-build-host-handoff.md | 777 ++++++++++++++++++ specs/001-enterprise-experience/plan.md | 42 +- specs/001-enterprise-experience/research.md | 26 + specs/001-enterprise-experience/spec.md | 128 +++ specs/001-enterprise-experience/tasks.md | 98 +++ .../test_activation_health_window.py | 97 +++ .../deployment/test_atomic_profile_journal.py | 74 ++ .../deployment/test_install_python_profile.py | 91 ++ tests/deployment/test_offline_payload_lock.py | 89 ++ .../test_prepare_python_profile_operation.py | 65 ++ .../test_python_environment_checkpoint.py | 74 ++ .../deployment/test_python_runtime_profile.py | 41 + .../test_python_runtime_profile_plan.py | 76 ++ .../test_recover_python_profile_on_boot.py | 48 ++ .../test_verify_python_profile_membership.py | 76 ++ 152 files changed, 9445 insertions(+), 828 deletions(-) create mode 100644 autoinstall/offline/activation-health-window.py create mode 100644 autoinstall/offline/atomic-profile-journal.py create mode 100644 autoinstall/offline/container-sources.json create mode 100644 autoinstall/offline/install-python-profile.py create mode 100644 autoinstall/offline/payload-lock.candidate.json create mode 100644 autoinstall/offline/plan-python-runtime-profile.py create mode 100644 autoinstall/offline/prepare-python-profile-operation.py create mode 100644 autoinstall/offline/python-environment-checkpoint.py create mode 100644 autoinstall/offline/python-runtime-profiles.json create mode 100644 autoinstall/offline/recover-python-profile-on-boot.py create mode 100644 autoinstall/offline/resolve-apt-lock.py create mode 100644 autoinstall/offline/resolve-payload-lock.py create mode 100644 autoinstall/offline/resolve-python-lock.py create mode 100644 autoinstall/offline/validate-payload-lock.py create mode 100644 autoinstall/offline/validate-python-runtime-profile.py create mode 100644 autoinstall/offline/verify-python-profile-membership.py create mode 100644 cezen-portal/app-studio.html create mode 100644 cezen-portal/applications.html create mode 100644 cezen-portal/dataset-creation.html create mode 100644 cezen-portal/nexus-compare.html create mode 100644 cezen-portal/nexus-notebook.html create mode 100644 docs/Nexus_One_AI_Product_Presentation.pptx.inspect.ndjson create mode 100644 how-it-flows.gif create mode 100644 release/1.0.0-rc1/README.md create mode 100644 release/1.0.0-rc1/agent-registry.schema.json create mode 100755 release/1.0.0-rc1/build-rc.sh create mode 100644 release/1.0.0-rc1/feature-profiles.json create mode 100755 release/1.0.0-rc1/make-trial.py create mode 100755 release/1.0.0-rc1/qualify.py create mode 100644 release/1.0.0-rc1/release.json create mode 100644 specs/001-enterprise-experience/analysis/gate-1-decision-dossier.md create mode 100644 specs/001-enterprise-experience/analysis/gate-evidence-allocation.md create mode 100644 specs/001-enterprise-experience/analysis/installer-release-consistency.md create mode 100644 specs/001-enterprise-experience/contracts/architecture-decisions.md create mode 100644 specs/001-enterprise-experience/contracts/compatibility-evaluation.md create mode 100644 specs/001-enterprise-experience/contracts/installer-artifact-contract.md create mode 100644 specs/001-enterprise-experience/contracts/installer-threat-model.md create mode 100644 specs/001-enterprise-experience/contracts/model-pack-lifecycle.md create mode 100644 specs/001-enterprise-experience/contracts/offline-license-policy.md create mode 100644 specs/001-enterprise-experience/contracts/offline-validation-lab.md create mode 100644 specs/001-enterprise-experience/contracts/online-universal-acquisition.md create mode 100644 specs/001-enterprise-experience/contracts/payload-inventory-sizing.md create mode 100644 specs/001-enterprise-experience/contracts/python-runtime-profile-selection.md create mode 100644 specs/001-enterprise-experience/contracts/release-lifecycle-provenance.md create mode 100644 specs/001-enterprise-experience/contracts/release-signing-trust.md create mode 100644 specs/001-enterprise-experience/contracts/schemas/artifact-manifest.schema.json create mode 100644 specs/001-enterprise-experience/contracts/schemas/profile-membership-verification-receipt.schema.json create mode 100644 specs/001-enterprise-experience/contracts/schemas/signature-envelope.schema.json create mode 100644 specs/001-enterprise-experience/contracts/systemd/README.md create mode 100644 specs/001-enterprise-experience/contracts/systemd/cezen-api.service create mode 100644 specs/001-enterprise-experience/contracts/systemd/chromadb.service create mode 100644 specs/001-enterprise-experience/contracts/systemd/nexus-one-activation-health.service create mode 100644 specs/001-enterprise-experience/contracts/systemd/nexus-one-activation-rollback.service create mode 100644 specs/001-enterprise-experience/contracts/systemd/nginx-cezen-ordering.conf create mode 100644 specs/001-enterprise-experience/contracts/test-vectors/README.md create mode 100644 specs/001-enterprise-experience/contracts/test-vectors/compatibility-cases.json create mode 100644 specs/001-enterprise-experience/contracts/test-vectors/ed25519-test-public.pem create mode 100644 specs/001-enterprise-experience/contracts/test-vectors/model-pack-manifest.valid.json create mode 100644 specs/001-enterprise-experience/contracts/test-vectors/model-pack-signature.valid.json create mode 100644 specs/001-enterprise-experience/contracts/test-vectors/python-profile-journal-resume.json create mode 100644 specs/001-enterprise-experience/contracts/test-vectors/python-profile-membership-receipt.structural-test.json create mode 100644 specs/001-enterprise-experience/contracts/test-vectors/python-profile-probe-gpu-lite.json create mode 100644 specs/001-enterprise-experience/contracts/test-vectors/signature-cases.json create mode 100644 specs/001-enterprise-experience/contracts/test-vectors/trust-bundle-manifest.valid.json create mode 100644 specs/001-enterprise-experience/contracts/test-vectors/trust-bundle-signature.valid.json create mode 100644 specs/001-enterprise-experience/contracts/test-vectors/update-bundle-manifest.valid.json create mode 100644 specs/001-enterprise-experience/contracts/test-vectors/update-bundle-signature.valid.json create mode 100644 specs/001-enterprise-experience/contracts/test-vectors/validate_vectors.py create mode 100644 specs/001-enterprise-experience/contracts/test-vectors/verification-evidence.md create mode 100644 specs/001-enterprise-experience/contracts/test-vectors/workstation-iso-manifest.valid.json create mode 100644 specs/001-enterprise-experience/contracts/test-vectors/workstation-iso-signature.valid.json create mode 100644 specs/001-enterprise-experience/contracts/update-rollback-policy.md create mode 100644 specs/001-enterprise-experience/contracts/workstation-hardware-model-baseline.md create mode 100644 specs/001-enterprise-experience/contracts/workstation-runtime-compatibility-baseline.md create mode 100644 specs/001-enterprise-experience/evidence/gate2-build-host-handoff.md create mode 100644 tests/deployment/test_activation_health_window.py create mode 100644 tests/deployment/test_atomic_profile_journal.py create mode 100644 tests/deployment/test_install_python_profile.py create mode 100644 tests/deployment/test_offline_payload_lock.py create mode 100644 tests/deployment/test_prepare_python_profile_operation.py create mode 100644 tests/deployment/test_python_environment_checkpoint.py create mode 100644 tests/deployment/test_python_runtime_profile.py create mode 100644 tests/deployment/test_python_runtime_profile_plan.py create mode 100644 tests/deployment/test_recover_python_profile_on_boot.py create mode 100644 tests/deployment/test_verify_python_profile_membership.py diff --git a/ansible/roles/cezen-backend/files/main.py b/ansible/roles/cezen-backend/files/main.py index 98f3fbd..83c3461 100644 --- a/ansible/roles/cezen-backend/files/main.py +++ b/ansible/roles/cezen-backend/files/main.py @@ -67,6 +67,7 @@ JWT_ALGORITHM = "HS256" JWT_EXPIRE_HRS = 8 COOKIE_NAME = "cezen_token" OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434") +PROTOTYPE_LICENSE_BYPASS = os.environ.get("CEZEN_PROTOTYPE_BYPASS_LICENSE", "").strip().lower() in {"1", "true", "yes"} SCHEMA_VERSION = 1 CUSTOMER_STATES = {"loading", "empty", "ready", "degraded", "failed", "restricted", "unavailable", "stale"} @@ -379,6 +380,33 @@ def init_db(): finished_at TEXT ); + CREATE TABLE IF NOT EXISTS dataset_generation_jobs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + source_name TEXT NOT NULL, + source_path TEXT NOT NULL, + model TEXT NOT NULL, + format TEXT NOT NULL DEFAULT 'qa', + target_count INTEGER NOT NULL DEFAULT 20, + status TEXT NOT NULL DEFAULT 'pending', + generated_count INTEGER NOT NULL DEFAULT 0, + error_msg TEXT, + created_by TEXT NOT NULL, + created_at TEXT NOT NULL, + started_at TEXT, + finished_at TEXT + ); + + CREATE TABLE IF NOT EXISTS dataset_generation_samples ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + job_id INTEGER NOT NULL, + instruction TEXT NOT NULL, + output TEXT NOT NULL, + source_text TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'pending', + created_at TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS prompts ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, @@ -1171,6 +1199,8 @@ def _feature_enabled(feature_key: str, feasibility: Optional[dict] = None) -> bo def require_feature(feature_key: str): def dependency(user: dict = Depends(current_user)): + if PROTOTYPE_LICENSE_BYPASS: + return user if not _feature_enabled(feature_key): raise HTTPException(status_code=403, detail=f"Feature '{feature_key}' is not licensed or provisioned") return user @@ -1571,12 +1601,14 @@ SERVICES = [ {"name": "jupyter", "port": 8888, "label": "JupyterLab"}, {"name": "mlflow", "port": 5000, "label": "MLflow"}, {"name": "grafana", "port": 3000, "label": "Grafana"}, + {"name": "k3s", "host": "192.168.122.1", "port": 6443, "label": "K3s"}, + {"name": "harbor", "host": "192.168.122.1", "port": 30443, "label": "Harbor Registry"}, ] def service_status(svc: dict) -> dict: import socket try: - s = socket.create_connection(("127.0.0.1", svc["port"]), timeout=1) + s = socket.create_connection((svc.get("host", "127.0.0.1"), svc["port"]), timeout=1) s.close() running = True except OSError: @@ -2634,25 +2666,261 @@ async def rag_query(body: dict, user: dict = Depends(current_user)): DATASETS_DIR = DATA_DIR / "datasets" MODELS_DIR = DATA_DIR / "finetuned" JOBS_LOG_DIR = DATA_DIR / "job_logs" +DATASET_SOURCES_DIR = DATA_DIR / "dataset_sources" DATASETS_DIR.mkdir(parents=True, exist_ok=True) MODELS_DIR.mkdir(parents=True, exist_ok=True) JOBS_LOG_DIR.mkdir(parents=True, exist_ok=True) +DATASET_SOURCES_DIR.mkdir(parents=True, exist_ok=True) -def _count_rows(path: Path, orig_name: str) -> int: - """Count data rows in JSONL or CSV file.""" - try: - if orig_name.lower().endswith(".csv"): - import csv - with open(path, newline="", encoding="utf-8", errors="replace") as f: - return max(0, sum(1 for _ in csv.reader(f)) - 1) # exclude header +_dataset_generation_tasks: set[asyncio.Task] = set() + +def _extract_dataset_source(path: Path, orig_name: str) -> str: + """Extract plain text from supported dataset source documents.""" + ext = Path(orig_name).suffix.lower() + if ext == ".pdf": + import fitz + with fitz.open(path) as doc: + return "\n\n".join(page.get_text("text") for page in doc) + if ext == ".docx": + from docx import Document + doc = Document(path) + return "\n".join(p.text for p in doc.paragraphs) + if ext == ".csv": + import csv + with open(path, newline="", encoding="utf-8", errors="replace") as f: + return "\n".join(" | ".join(str(v) for v in row) for row in csv.reader(f)) + return path.read_text(encoding="utf-8", errors="replace") + +def _chunk_dataset_source(text: str, max_chars: int = 6000) -> list[str]: + paragraphs = [" ".join(p.split()) for p in text.splitlines() if p.strip()] + chunks, current = [], "" + for paragraph in paragraphs: + if current and len(current) + len(paragraph) + 1 > max_chars: + chunks.append(current) + current = "" + if len(paragraph) > max_chars: + if current: + chunks.append(current) + current = "" + chunks.extend(paragraph[i:i + max_chars] for i in range(0, len(paragraph), max_chars)) else: - return sum(1 for line in open(path, encoding="utf-8", errors="replace") if line.strip()) - except Exception: - return 0 + current = f"{current}\n{paragraph}".strip() + if current: + chunks.append(current) + return chunks + +def _parse_generated_samples(raw: str) -> list[dict]: + """Accept a JSON array or an object containing a samples array.""" + raw = raw.strip() + if raw.startswith("```"): + raw = raw.split("\n", 1)[-1].rsplit("```", 1)[0].strip() + try: + data = json.loads(raw) + except json.JSONDecodeError: + start, end = raw.find("["), raw.rfind("]") + if start < 0 or end <= start: + return [] + data = json.loads(raw[start:end + 1]) + if isinstance(data, dict): + data = data.get("samples", []) + if not isinstance(data, list): + return [] + valid = [] + for row in data: + if not isinstance(row, dict): + continue + instruction = str(row.get("instruction") or row.get("question") or row.get("prompt") or "").strip() + output = str(row.get("output") or row.get("answer") or row.get("completion") or "").strip() + if instruction and output: + valid.append({"instruction": instruction[:8000], "output": output[:16000]}) + return valid + +def _generate_samples_for_chunk(model: str, source: str, count: int, output_format: str) -> list[dict]: + format_guidance = { + "qa": "Create grounded question and answer pairs.", + "instruction": "Create practical instruction and response pairs.", + "summary": "Create summarisation instructions and faithful summaries.", + }.get(output_format, "Create grounded question and answer pairs.") + prompt = f"""You create high-quality supervised fine-tuning data. {format_guidance} +Use only facts present in SOURCE. Do not invent facts. Make each example distinct and useful. +Return only a JSON array with exactly {count} objects. Each object must have string fields +\"instruction\" and \"output\". Do not use markdown fences. + +SOURCE: +{source} +""" + raw = _call_ollama_sync(model, prompt) + return _parse_generated_samples(raw) + +async def _run_dataset_generation(job_id: int): + db = get_db() + job = db.execute("SELECT * FROM dataset_generation_jobs WHERE id=?", (job_id,)).fetchone() + if not job: + db.close() + return + db.execute("UPDATE dataset_generation_jobs SET status='running', started_at=? WHERE id=?", (utcnow(), job_id)) + db.commit() + db.close() + try: + text = await asyncio.to_thread(_extract_dataset_source, Path(job["source_path"]), job["source_name"]) + chunks = _chunk_dataset_source(text) + if not chunks: + raise ValueError("No readable text was found in the source document") + target = int(job["target_count"]) + generated, seen = 0, set() + for index, chunk in enumerate(chunks): + remaining = target - generated + if remaining <= 0: + break + remaining_chunks = max(1, len(chunks) - index) + count = min(5, max(1, (remaining + remaining_chunks - 1) // remaining_chunks)) + rows = await asyncio.to_thread(_generate_samples_for_chunk, job["model"], chunk, count, job["format"]) + db = get_db() + for row in rows: + key = row["instruction"].casefold() + if key in seen or generated >= target: + continue + seen.add(key) + db.execute( + "INSERT INTO dataset_generation_samples (job_id,instruction,output,source_text,status,created_at) VALUES (?,?,?,?,?,?)", + (job_id, row["instruction"], row["output"], chunk[:6000], "pending", utcnow()) + ) + generated += 1 + db.execute("UPDATE dataset_generation_jobs SET generated_count=? WHERE id=?", (generated, job_id)) + db.commit() + db.close() + db = get_db() + db.execute("UPDATE dataset_generation_jobs SET status='completed', generated_count=?, finished_at=? WHERE id=?", (generated, utcnow(), job_id)) + db.commit() + db.close() + except Exception as exc: + db = get_db() + db.execute("UPDATE dataset_generation_jobs SET status='failed', error_msg=?, finished_at=? WHERE id=?", (str(exc)[:1000], utcnow(), job_id)) + db.commit() + db.close() + +@app.post("/api/training/dataset-generation") +async def create_dataset_generation( + file: UploadFile = File(...), name: str = Form(...), model: str = Form(...), + format: str = Form("qa"), target_count: int = Form(20), + admin: dict = Depends(admin_only) +): + allowed_ext = {".pdf", ".docx", ".txt", ".md", ".csv"} + orig = Path(file.filename or "source.txt").name + ext = Path(orig).suffix.lower() + if ext not in allowed_ext: + raise HTTPException(status_code=400, detail="Use PDF, DOCX, TXT, Markdown, or CSV source files") + if format not in {"qa", "instruction", "summary"}: + raise HTTPException(status_code=400, detail="Unsupported dataset format") + if not model.strip() or not name.strip(): + raise HTTPException(status_code=400, detail="Dataset name and local model are required") + target_count = max(1, min(int(target_count), 200)) + data = await file.read() + if not data or len(data) > 20 * 1024 * 1024: + raise HTTPException(status_code=400, detail="Source file must be between 1 byte and 20 MB") + source_path = DATASET_SOURCES_DIR / f"{uuid.uuid4().hex}{ext}" + source_path.write_bytes(data) + db = get_db() + cur = db.execute( + "INSERT INTO dataset_generation_jobs (name,source_name,source_path,model,format,target_count,status,created_by,created_at) VALUES (?,?,?,?,?,?,?,?,?)", + (name.strip()[:160], orig, str(source_path), model.strip()[:160], format, target_count, "pending", admin["username"], utcnow()) + ) + db.commit(); job_id = cur.lastrowid; db.close() + task = asyncio.create_task(_run_dataset_generation(job_id)) + _dataset_generation_tasks.add(task) + task.add_done_callback(_dataset_generation_tasks.discard) + _audit(None, admin["username"], "dataset_generation_start", f"Started dataset generation: {name} using {model}") + return {"job_id": job_id, "status": "pending"} + +@app.get("/api/training/dataset-generation") +async def list_dataset_generations(admin: dict = Depends(admin_only)): + db = get_db() + rows = db.execute("SELECT id,name,source_name,model,format,target_count,generated_count,status,error_msg,created_at,finished_at FROM dataset_generation_jobs ORDER BY id DESC").fetchall() + db.close() + return {"jobs": [dict(row) for row in rows]} + +@app.get("/api/training/dataset-generation/{job_id}/samples") +async def list_generated_samples(job_id: int, admin: dict = Depends(admin_only)): + db = get_db() + rows = db.execute("SELECT id,instruction,output,source_text,status FROM dataset_generation_samples WHERE job_id=? ORDER BY id", (job_id,)).fetchall() + db.close() + return {"samples": [dict(row) for row in rows]} + +@app.patch("/api/training/dataset-generation/samples/{sample_id}") +async def update_generated_sample(sample_id: int, body: dict, admin: dict = Depends(admin_only)): + status_value = body.get("status", "pending") + if status_value not in {"pending", "approved", "rejected"}: + raise HTTPException(status_code=400, detail="Invalid review status") + instruction, output = str(body.get("instruction", "")).strip(), str(body.get("output", "")).strip() + if not instruction or not output: + raise HTTPException(status_code=400, detail="Instruction and output cannot be empty") + db = get_db() + cur = db.execute("UPDATE dataset_generation_samples SET instruction=?,output=?,status=? WHERE id=?", (instruction[:8000], output[:16000], status_value, sample_id)) + db.commit(); db.close() + if not cur.rowcount: + raise HTTPException(status_code=404, detail="Sample not found") + return {"ok": True} + +@app.post("/api/training/dataset-generation/{job_id}/publish") +async def publish_generated_dataset(job_id: int, admin: dict = Depends(admin_only)): + db = get_db() + job = db.execute("SELECT * FROM dataset_generation_jobs WHERE id=?", (job_id,)).fetchone() + if not job: + db.close(); raise HTTPException(status_code=404, detail="Generation job not found") + rows = db.execute("SELECT instruction,output FROM dataset_generation_samples WHERE job_id=? AND status='approved' ORDER BY id", (job_id,)).fetchall() + if not rows: + db.close(); raise HTTPException(status_code=400, detail="Approve at least one sample before publishing") + filename = f"{uuid.uuid4().hex}.jsonl" + dest = DATASETS_DIR / filename + content = "".join(json.dumps({"instruction": row["instruction"], "output": row["output"]}, ensure_ascii=False) + "\n" for row in rows) + dest.write_text(content, encoding="utf-8") + orig_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in job["name"]).strip("_") or "generated_dataset" + orig_name += ".jsonl" + cur = db.execute("INSERT INTO training_datasets (filename,orig_name,file_path,size_bytes,row_count,uploaded_at) VALUES (?,?,?,?,?,?)", (filename, orig_name, str(dest), len(content.encode()), len(rows), utcnow())) + db.commit(); dataset_id = cur.lastrowid; db.close() + _audit(None, admin["username"], "dataset_generation_publish", f"Published {orig_name} ({len(rows)} approved rows)") + return {"dataset_id": dataset_id, "filename": orig_name, "row_count": len(rows)} + +def _validate_training_dataset(path: Path, orig_name: str) -> int: + """Validate a training dataset and return its semantic record count.""" + import csv + + ext = Path(orig_name).suffix.lower() + if ext == ".csv": + with open(path, newline="", encoding="utf-8", errors="replace") as f: + rows = list(csv.DictReader(f)) + elif ext == ".json": + with open(path, encoding="utf-8", errors="replace") as f: + payload = json.load(f) + if isinstance(payload, list): + rows = payload + elif isinstance(payload, dict): + rows = next((payload[key] for key in ("samples", "data", "records") + if isinstance(payload.get(key), list)), None) + if rows is None: + raise ValueError("JSON must be an array of training records or contain a samples, data, or records array") + else: + raise ValueError("JSON must contain an array of training records") + else: + rows = [] + with open(path, encoding="utf-8", errors="replace") as f: + for line_number, line in enumerate(f, 1): + if not line.strip(): + continue + try: + rows.append(json.loads(line)) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON on line {line_number}") from exc + + if not rows: + raise ValueError("Dataset has no training records") + if not all(isinstance(row, dict) and row for row in rows): + raise ValueError("Every training record must be a non-empty object or CSV row") + return len(rows) @app.post("/api/training/datasets") -async def upload_dataset(file: UploadFile = File(...), _licensed: dict = Depends(require_feature("fine_tuning")), admin: dict = Depends(admin_only)): +async def upload_dataset(file: UploadFile = File(...), admin: dict = Depends(admin_only)): """Upload a JSONL or CSV dataset for fine-tuning.""" allowed_ext = {".jsonl", ".json", ".csv"} orig = file.filename or "dataset" @@ -2668,7 +2936,11 @@ async def upload_dataset(file: UploadFile = File(...), _licensed: dict = Depends dest = DATASETS_DIR / filename dest.write_bytes(data) - row_count = _count_rows(dest, orig) + try: + row_count = _validate_training_dataset(dest, orig) + except (ValueError, json.JSONDecodeError) as exc: + dest.unlink(missing_ok=True) + raise HTTPException(status_code=400, detail=f"Invalid training dataset: {exc}") from exc db = get_db() cur = db.execute( @@ -2683,7 +2955,7 @@ async def upload_dataset(file: UploadFile = File(...), _licensed: dict = Depends return {"id": row_id, "filename": orig, "size_bytes": len(data), "row_count": row_count} @app.get("/api/training/datasets") -async def list_datasets(_licensed: dict = Depends(require_feature("fine_tuning")), admin: dict = Depends(admin_only)): +async def list_datasets(admin: dict = Depends(admin_only)): db = get_db() rows = db.execute( "SELECT id, orig_name, size_bytes, row_count, uploaded_at FROM training_datasets ORDER BY uploaded_at DESC" @@ -2692,7 +2964,7 @@ async def list_datasets(_licensed: dict = Depends(require_feature("fine_tuning") return {"datasets": [dict(r) for r in rows]} @app.delete("/api/training/datasets/{dataset_id}") -async def delete_dataset(dataset_id: int, _licensed: dict = Depends(require_feature("fine_tuning")), admin: dict = Depends(admin_only)): +async def delete_dataset(dataset_id: int, admin: dict = Depends(admin_only)): db = get_db() row = db.execute("SELECT * FROM training_datasets WHERE id=?", (dataset_id,)).fetchone() if not row: @@ -2726,6 +2998,11 @@ async def launch_job(body: dict, _licensed: dict = Depends(require_feature("fine if not ds: db.close() raise HTTPException(status_code=404, detail="Dataset not found") + try: + _validate_training_dataset(Path(ds["file_path"]), ds["orig_name"]) + except (OSError, ValueError, json.JSONDecodeError) as exc: + db.close() + raise HTTPException(status_code=400, detail=f"Invalid training dataset: {exc}") from exc config = { "epochs": int(body.get("epochs", 3)), @@ -3402,6 +3679,10 @@ async def my_analytics(days: int = 30, user: dict = Depends(current_user)): AGENT_STEP_TYPES = {"prompt", "summarise", "extract", "classify", "rag", "format"} +# Keep strong references to in-flight tasks. asyncio only maintains weak +# references, so fire-and-forget tasks can otherwise disappear mid-run. +_agent_run_tasks: set = set() + def _call_ollama_sync(model: str, prompt: str) -> str: import urllib.request as _ur routed = _select_router_model(prompt, model, {"content_type": "text"}) @@ -3626,7 +3907,9 @@ async def run_agent(agent_id: int, body: dict, user: dict = Depends(current_user ) run_id = cur.lastrowid db.commit(); db.close() - asyncio.create_task(_execute_agent(run_id)) + task = asyncio.create_task(_execute_agent(run_id)) + _agent_run_tasks.add(task) + task.add_done_callback(_agent_run_tasks.discard) return {"run_id": run_id, "status": "pending"} @@ -4317,6 +4600,7 @@ Respond with ONLY valid JSON, no markdown, no explanation: payload = { "model": judge_model, "messages": [{"role": "user", "content": judge_prompt}], + "format": "json", "stream": False, } try: @@ -4595,13 +4879,13 @@ async def get_eval_results(run_id: int, user=Depends(current_user)): # ── Scheduled Jobs ──────────────────────────────────────────────────────────── -def _run_scheduled_job_sync(job_id: int): +def _run_scheduled_job_sync(job_id: int, force: bool = False): """Execute a scheduled job (runs in executor thread).""" import asyncio as _asyncio db = get_db() try: job = db.execute("SELECT * FROM scheduled_jobs WHERE id=?", (job_id,)).fetchone() - if not job or not job["is_active"]: + if not job or (not job["is_active"] and not force): db.close() return @@ -4842,7 +5126,8 @@ async def trigger_schedule(job_id: int, user=Depends(current_user)): if not row: raise HTTPException(404, "Job not found") loop = asyncio.get_event_loop() - loop.run_in_executor(None, _run_scheduled_job_sync, job_id) + # A manual run is explicit user intent and must also work for paused jobs. + loop.run_in_executor(None, _run_scheduled_job_sync, job_id, True) return {"ok": True, "message": "Job triggered"} diff --git a/ansible/roles/cezen-backend/files/train_qlora.py b/ansible/roles/cezen-backend/files/train_qlora.py index 4e974c3..c8e71c5 100644 --- a/ansible/roles/cezen-backend/files/train_qlora.py +++ b/ansible/roles/cezen-backend/files/train_qlora.py @@ -73,7 +73,7 @@ def set_status(status: str): # ── Dataset loading ─────────────────────────────────────────────────────────── def load_dataset_from_file(path: str): - """Load JSONL or CSV dataset into a list of dicts with 'text' or 'prompt'/'completion' keys.""" + """Load JSON, JSONL, or CSV training records as a list of objects.""" p = Path(path) rows = [] if p.suffix.lower() == ".csv": @@ -82,6 +82,16 @@ def load_dataset_from_file(path: str): reader = csv.DictReader(f) for row in reader: rows.append(dict(row)) + elif p.suffix.lower() == ".json": + with open(path, encoding="utf-8", errors="replace") as f: + payload = json.load(f) + if isinstance(payload, list): + rows = payload + elif isinstance(payload, dict): + for key in ("samples", "data", "records"): + if isinstance(payload.get(key), list): + rows = payload[key] + break else: with open(path, encoding="utf-8", errors="replace") as f: for line in f: @@ -90,7 +100,11 @@ def load_dataset_from_file(path: str): try: rows.append(json.loads(line)) except Exception: - pass + raise ValueError("Dataset contains an invalid JSONL row") + if not rows: + raise ValueError("Dataset has no training records") + if not all(isinstance(row, dict) for row in rows): + raise ValueError("Every training record must be a JSON object or CSV row") return rows def format_row(row: dict) -> str: diff --git a/autoinstall/offline/README.md b/autoinstall/offline/README.md index 15fa77b..252d11a 100644 --- a/autoinstall/offline/README.md +++ b/autoinstall/offline/README.md @@ -1,8 +1,9 @@ -# Nexus One AI Offline Server Payload +# Nexus One AI Offline Workstation Payload -This directory defines the payload embedded by `build-iso-offline.sh`. The -offline installer is deliberately separate from the existing online bootstrap -ISO until disconnected clean-install validation is complete. +This directory contains Gate 2 Offline Workstation payload-resolution and disposable-validation +tools. **It does not currently contain a lock-driven bundle assembler or `build-iso-offline.sh`.** +No command in this directory produces a release ISO, and the existing mutable body in +`build-bundle.sh` is deliberately unreachable. The agreed bootable release matrix is: @@ -30,15 +31,76 @@ updates are periodically rolled into refreshed ISO builds. Server L/Max models are intentionally separate signed model packs. Optional external connectors cannot operate without a customer-approved route to their target. -## Build stages +## Current operator flow -1. On a connected Ubuntu 22.04 staging host with at least 80 GiB free, run - `sudo bash autoinstall/offline/build-bundle.sh`. -2. Review `autoinstall/offline-output/manifest/` and sign - `SHA256SUMS` with the release key. -3. Run `sudo bash autoinstall/build-iso-offline.sh` to inject the verified - payload into the Ubuntu Server ISO. -4. Validate with all network interfaces disconnected. +The only authorized/current flow is: -The bundle builder fails closed when an expected payload is absent. An ISO must -not be labelled offline merely because the source tree was embedded. +1. Resolve candidate inventory on a connected, isolated staging host. +2. Review candidate object identity, bytes, origin, license and unresolved entries. +3. Validate the candidate lock; it MUST fail while status is not `reviewed` or unresolved entries exist. +4. Exercise signed-profile, checkpoint, install and rollback components only against disposable targets. +5. Stop. Assembly, ISO injection, signing and release validation are not implemented. + +| Stage | Tool | Current status | +|---|---|---| +| Source/OCI candidate resolution | `resolve-payload-lock.py` | Evidence-only; OCI archive bytes/licenses remain open | +| Clean APT closure | `resolve-apt-lock.py` | Evidence-only; repository snapshot/origin/licenses and NVIDIA APT closure remain open | +| Backend/Chroma/platform wheel closures | `resolve-python-lock.py` | Evidence-only; three independent environments, candidate lock only | +| Whole-lock preflight | `validate-payload-lock.py` | Available and fail-closed | +| CPU/NVIDIA profile bytes | `validate-python-runtime-profile.py` | Available for exact candidate directories | +| Signed membership | `verify-python-profile-membership.py` | Implemented/test-key proven; production trust inputs absent | +| Selection planning | `plan-python-runtime-profile.py` | Non-mutating; production receipt required by default | +| Atomic journal/checkpoint preparation | `prepare-python-profile-operation.py` | Disposable/pre-mutation evidence only | +| Offline Python mutation/rollback | `install-python-profile.py` | Disposable targets only; not installer-wired | +| Bundle assembly | `build-bundle.sh` | Hard blocked after lock validation; legacy body unreachable | +| Offline ISO build | `autoinstall/build-iso-offline.sh` | Absent; C1 readiness blocker | + +## Candidate resolution stages + +1. Resolve `payload-lock.candidate.json` into a reviewed `payload-lock.json` containing every object + version, architecture, source, target, byte size, SHA-256 and license. The candidate intentionally + fails validation while any entry is unresolved. + The first connected resolution stage is: + `python3 autoinstall/offline/resolve-payload-lock.py --candidate + autoinstall/offline/payload-lock.candidate.json --source-root . --container-sources + autoinstall/offline/container-sources.json --staging --output `. + It creates a deterministic source archive and resolves the Linux/AMD64 OCI manifests without + pulling image layers. Registry-declared bytes are not final measured payload bytes. + Resolve the Ubuntu package closure from an empty package state with + `python3 autoinstall/offline/resolve-apt-lock.py --lock --packages + autoinstall/offline/apt-packages.txt --work --output `. + The result records exact `.deb` bytes but remains unresolved until repository snapshot/origin, + license/notice, and the separate NVIDIA R570/CUDA 12.8.1 closure are approved. + Resolve the three independent Python 3.11 wheel closures with + `python3 autoinstall/offline/resolve-python-lock.py --lock + --backend-requirements ansible/roles/cezen-backend/files/requirements.txt + --platform-requirements autoinstall/offline/python-packages.txt --work + --output `. The resolver pins its own pip version, accepts binary wheels only, + and hashes every wheel. Index provenance, licenses, offline install/import and CUDA compatibility + remain review gates. +2. Run `python3 autoinstall/offline/validate-payload-lock.py + autoinstall/offline/payload-lock.json`. No acquisition or output deletion occurs before this passes. +3. Do not run `build-bundle.sh` expecting output. It validates the lock and then stops because + lock-driven acquisition is not implemented. It will not fall back to the legacy mutable path. + +## Disposable profile evidence + +- `python-runtime-profiles.json` defines exactly-one `cpu-only` or `nvidia-cu128` selection. +- `verify-python-profile-membership.py` emits the closed receipt consumed by the planner; structural + test receipts require an explicit test-only switch and are rejected in production mode. +- `prepare-python-profile-operation.py` validates membership and wheel bytes, writes the atomic journal, + creates/verifies the checkpoint and stops at `checkpoint_ready` without mutation. +- `install-python-profile.py` installs from a selected local wheel directory with `--no-index`, performs + health checks, atomically activates, and supports checkpoint rollback. It is currently authorized + only for disposable evidence paths. + +## Mandatory stop conditions + +Stop without assembly or mutation when any of the following is true: lock status is not `reviewed`, +unresolved entries remain, a required object/hash/profile membership is missing, signature/trust policy +is unverified, the journal/checkpoint is invalid, the selected hardware profile is unsupported, or the +target is not an explicitly disposable evidence path. + +An ISO must not be labelled offline merely because source or candidate payload bytes were embedded. +Only a future signed, lock-driven artifact that passes the full disconnected acceptance matrix may use +the Offline Workstation release label. diff --git a/autoinstall/offline/activation-health-window.py b/autoinstall/offline/activation-health-window.py new file mode 100644 index 0000000..3ed79db --- /dev/null +++ b/autoinstall/offline/activation-health-window.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +"""Evaluate a bounded, semantic, loopback-only activation health window.""" + +from __future__ import annotations + +import argparse +import hashlib +import ipaddress +import json +import os +import re +import subprocess +import tempfile +import time +import urllib.error +import urllib.parse +import urllib.request +from pathlib import Path +from typing import Callable + + +SCHEMA = "com.cezen.nexus-one.activation-health-evidence.v1" +MAX_RESPONSE_BYTES = 65536 +UNIT_NAME = re.compile(r"^[A-Za-z0-9_.@-]+\.service$") + + +class HealthConfigError(ValueError): + pass + + +def canonical(value: object) -> bytes: + return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode() + + +def validate_config(config: dict) -> list[dict]: + if set(config) != {"schema", "components"} or config["schema"] != \ + "com.cezen.nexus-one.activation-health-profile.v1": + raise HealthConfigError("HEALTH_PROFILE_INVALID") + components = config["components"] + if not isinstance(components, list) or not components: + raise HealthConfigError("HEALTH_COMPONENTS_INVALID") + names = set() + result = [] + for component in components: + if not isinstance(component, dict) or set(component) != { + "name", "url", "required", "expected_service", "systemd_unit", "max_restarts"}: + raise HealthConfigError("HEALTH_COMPONENT_INVALID") + name = component["name"] + if not isinstance(name, str) or not name or name in names: + raise HealthConfigError("HEALTH_COMPONENT_NAME_INVALID") + names.add(name) + if not isinstance(component["required"], bool): + raise HealthConfigError("HEALTH_COMPONENT_REQUIRED_INVALID") + expected = component["expected_service"] + if not isinstance(expected, str) or not expected: + raise HealthConfigError("HEALTH_COMPONENT_EXPECTED_SERVICE_INVALID") + unit = component["systemd_unit"] + if unit is not None and (not isinstance(unit, str) or not UNIT_NAME.fullmatch(unit)): + raise HealthConfigError("HEALTH_COMPONENT_SYSTEMD_UNIT_INVALID") + maximum = component["max_restarts"] + if not isinstance(maximum, int) or isinstance(maximum, bool) or maximum < 0 or maximum > 1000: + raise HealthConfigError("HEALTH_COMPONENT_MAX_RESTARTS_INVALID") + parsed = urllib.parse.urlparse(component["url"]) + if parsed.scheme != "http" or parsed.username or parsed.password or parsed.query \ + or parsed.fragment or not parsed.port or parsed.path == "": + raise HealthConfigError("HEALTH_URL_INVALID") + try: + address = ipaddress.ip_address(parsed.hostname or "") + except ValueError as error: + raise HealthConfigError("HEALTH_URL_NOT_LITERAL_LOOPBACK") from error + if not address.is_loopback: + raise HealthConfigError("HEALTH_URL_NOT_LOOPBACK") + result.append(dict(component)) + return result + + +def fetch(component: dict, timeout_seconds: float) -> dict: + request = urllib.request.Request(component["url"], headers={"Accept": "application/json"}) + try: + with urllib.request.urlopen(request, timeout=timeout_seconds) as response: + if response.status != 200: + return {"ok": False, "code": f"HTTP_{response.status}"} + content_type = response.headers.get_content_type() + if content_type != "application/json": + return {"ok": False, "code": "CONTENT_TYPE_INVALID"} + payload = response.read(MAX_RESPONSE_BYTES + 1) + if len(payload) > MAX_RESPONSE_BYTES: + return {"ok": False, "code": "RESPONSE_TOO_LARGE"} + except urllib.error.HTTPError as error: + return {"ok": False, "code": f"HTTP_{error.code}"} + except TimeoutError: + return {"ok": False, "code": "TIMEOUT"} + except (OSError, urllib.error.URLError): + return {"ok": False, "code": "UNREACHABLE"} + try: + decoded = json.loads(payload) + except (UnicodeDecodeError, json.JSONDecodeError): + return {"ok": False, "code": "MALFORMED_JSON"} + if not isinstance(decoded, dict) or set(decoded) != {"service", "status"}: + return {"ok": False, "code": "RESPONSE_SHAPE_INVALID"} + if decoded["service"] != component["expected_service"]: + return {"ok": False, "code": "SERVICE_IDENTITY_MISMATCH"} + if decoded["status"] not in {"ready", "degraded", "failed"}: + return {"ok": False, "code": "STATUS_INVALID"} + return {"ok": decoded["status"] == "ready", "code": decoded["status"].upper(), + "status": decoded["status"]} + + +def restart_state(component: dict) -> dict: + unit = component["systemd_unit"] + if unit is None: + return {"ok": True, "code": "RESTART_CHECK_NOT_APPLICABLE", "restarts": 0} + try: + process = subprocess.run( + ["systemctl", "show", unit, "--property=NRestarts", "--property=ActiveState", + "--property=SubState", "--no-pager"], check=True, capture_output=True, text=True, + timeout=5, + ) + except (OSError, subprocess.SubprocessError): + return {"ok": False, "code": "UNIT_STATE_UNAVAILABLE", "restarts": None} + values = {} + for line in process.stdout.splitlines(): + key, separator, value = line.partition("=") + if not separator or key in values: + return {"ok": False, "code": "UNIT_STATE_INVALID", "restarts": None} + values[key] = value + if set(values) != {"NRestarts", "ActiveState", "SubState"} or not values["NRestarts"].isdigit(): + return {"ok": False, "code": "UNIT_STATE_INVALID", "restarts": None} + restarts = int(values["NRestarts"]) + if restarts > component["max_restarts"]: + return {"ok": False, "code": "RESTART_STORM", "restarts": restarts} + if values["ActiveState"] != "active": + return {"ok": False, "code": "UNIT_NOT_ACTIVE", "restarts": restarts} + return {"ok": True, "code": "RESTARTS_OK", "restarts": restarts} + + +def atomic_evidence(path: Path, evidence: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) + try: + os.fchmod(fd, 0o600) + with os.fdopen(fd, "wb") as stream: + stream.write(canonical(evidence)) + stream.flush() + os.fsync(stream.fileno()) + os.replace(temporary, path) + directory_fd = os.open(path.parent, os.O_RDONLY) + try: + os.fsync(directory_fd) + finally: + os.close(directory_fd) + finally: + if os.path.exists(temporary): + os.unlink(temporary) + + +def evaluate(config: dict, operation_id: str, samples: int, interval_seconds: float, + timeout_seconds: float, window_timeout_seconds: float, allow_degraded: bool, + fetcher: Callable = fetch, restart_checker: Callable = restart_state, + sleeper: Callable = time.sleep, clock: Callable = time.monotonic) -> dict: + components = validate_config(config) + if not operation_id or any(character not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_." for character in operation_id): + raise HealthConfigError("OPERATION_ID_INVALID") + if samples < 1 or samples > 100 or interval_seconds < 0 or timeout_seconds <= 0 \ + or window_timeout_seconds <= 0 or timeout_seconds > window_timeout_seconds: + raise HealthConfigError("HEALTH_WINDOW_BOUNDS_INVALID") + records = [] + outcome = "ready" + deadline = clock() + window_timeout_seconds + for sample in range(1, samples + 1): + for component in components: + remaining = deadline - clock() + if remaining <= 0: + result = {"ok": False, "code": "WINDOW_TIMEOUT"} + restart = {"code": "NOT_CHECKED", "restarts": None} + else: + restart = restart_checker(component) + if restart.get("ok") is not True: + result = restart + else: + remaining = deadline - clock() + result = {"ok": False, "code": "WINDOW_TIMEOUT"} if remaining <= 0 else \ + fetcher(component, min(timeout_seconds, remaining)) + if clock() > deadline: + result = {"ok": False, "code": "WINDOW_TIMEOUT"} + status = result.get("status") + accepted_degraded = status == "degraded" and not component["required"] and allow_degraded + accepted = result.get("ok") is True or accepted_degraded + if accepted_degraded: + outcome = "degraded" + records.append({"sample": sample, "component": component["name"], + "required": component["required"], "accepted": accepted, + "code": result.get("code", "PROBE_RESULT_INVALID"), + "restart_code": restart.get("code"), + "restart_count": restart.get("restarts")}) + if not accepted: + outcome = "rejected" + return {"schema": SCHEMA, "operation_id": operation_id, + "profile_sha256": hashlib.sha256(canonical(config)).hexdigest(), + "samples_required": samples, "samples_completed": sample, + "allow_degraded": allow_degraded, "outcome": outcome, "checks": records} + if sample != samples: + if clock() + interval_seconds > deadline: + records.append({"sample": sample, "component": "window", "required": True, + "accepted": False, "code": "WINDOW_TIMEOUT", + "restart_code": "NOT_APPLICABLE", "restart_count": None}) + return {"schema": SCHEMA, "operation_id": operation_id, + "profile_sha256": hashlib.sha256(canonical(config)).hexdigest(), + "samples_required": samples, "samples_completed": sample, + "allow_degraded": allow_degraded, "outcome": "rejected", + "checks": records} + sleeper(interval_seconds) + return {"schema": SCHEMA, "operation_id": operation_id, + "profile_sha256": hashlib.sha256(canonical(config)).hexdigest(), + "samples_required": samples, "samples_completed": samples, + "allow_degraded": allow_degraded, "outcome": outcome, "checks": records} + + +def main(argv: list[str] | None = None, fetcher: Callable = fetch, + restart_checker: Callable = restart_state, sleeper: Callable = time.sleep, + clock: Callable = time.monotonic) -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--operation-id", required=True) + parser.add_argument("--profile-config", type=Path, required=True) + parser.add_argument("--samples", type=int, required=True) + parser.add_argument("--interval-seconds", type=float, required=True) + parser.add_argument("--timeout-seconds", type=float, required=True) + parser.add_argument("--window-timeout-seconds", type=float, required=True) + parser.add_argument("--evidence", type=Path, required=True) + parser.add_argument("--allow-degraded", action="store_true") + args = parser.parse_args(argv) + try: + config = json.loads(args.profile_config.read_text(encoding="utf-8")) + evidence = evaluate(config, args.operation_id, args.samples, args.interval_seconds, + args.timeout_seconds, args.window_timeout_seconds, args.allow_degraded, + fetcher, restart_checker, sleeper, clock) + atomic_evidence(args.evidence, evidence) + except (HealthConfigError, OSError, json.JSONDecodeError) as error: + print(json.dumps({"accepted": False, "code": str(error)}, sort_keys=True)) + return 2 + print(json.dumps({"accepted": evidence["outcome"] in {"ready", "degraded"}, + "outcome": evidence["outcome"]}, sort_keys=True)) + return 0 if evidence["outcome"] in {"ready", "degraded"} else 23 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/autoinstall/offline/atomic-profile-journal.py b/autoinstall/offline/atomic-profile-journal.py new file mode 100644 index 0000000..48e875c --- /dev/null +++ b/autoinstall/offline/atomic-profile-journal.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +"""Atomically persist the Python profile operation journal.""" + +from __future__ import annotations + +import argparse +import fcntl +import hashlib +import json +import os +import tempfile +from pathlib import Path + + +class JournalError(ValueError): + pass + + +TRANSITIONS = { + None: {"profile_selected"}, + "profile_selected": {"checkpoint_ready", "rolled_back"}, + "checkpoint_ready": {"installing", "rolled_back"}, + "installing": {"installed", "rollback_required"}, + "rollback_required": {"rolled_back"}, + "installed": set(), + "rolled_back": set(), +} + + +def canonical(record: dict) -> bytes: + return (json.dumps(record, sort_keys=True, separators=(",", ":")) + "\n").encode() + + +def digest_record(record: dict) -> str: + unsigned = {key: value for key, value in record.items() if key != "record_sha256"} + return hashlib.sha256(canonical(unsigned)).hexdigest() + + +def read_verified(path: Path) -> dict | None: + if not path.exists(): + return None + record = json.loads(path.read_text(encoding="utf-8")) + if record.get("record_sha256") != digest_record(record): + raise JournalError("JOURNAL_DIGEST_INVALID") + return record + + +def next_record(current: dict | None, expected_generation: int, operation_id: str, + profile: str, manifest_sha256: str, state: str, + checkpoint_id: str | None) -> dict: + generation = current.get("generation", 0) if current else 0 + if generation != expected_generation: + raise JournalError("JOURNAL_GENERATION_STALE") + prior_state = current.get("state") if current else None + if state not in TRANSITIONS.get(prior_state, set()): + raise JournalError("JOURNAL_TRANSITION_INVALID") + if current and (current["operation_id"] != operation_id or current["selected_profile"] != profile + or current["manifest_sha256"] != manifest_sha256): + raise JournalError("JOURNAL_IDENTITY_CHANGED") + inherited_checkpoint = current.get("checkpoint_id") if current else None + checkpoint = checkpoint_id or inherited_checkpoint + if state in {"checkpoint_ready", "installing", "installed", "rollback_required", "rolled_back"} \ + and prior_state != "profile_selected" and not checkpoint: + raise JournalError("CHECKPOINT_ID_REQUIRED") + if state in {"checkpoint_ready", "installing", "installed", "rollback_required"} and not checkpoint: + raise JournalError("CHECKPOINT_ID_REQUIRED") + record = { + "schema": "com.cezen.nexus-one.python-profile-journal.v1", + "generation": generation + 1, + "operation_id": operation_id, + "selected_profile": profile, + "manifest_sha256": manifest_sha256, + "state": state, + "checkpoint_id": checkpoint, + "previous_record_sha256": current.get("record_sha256") if current else None, + } + record["record_sha256"] = digest_record(record) + return record + + +def atomic_write(path: Path, record: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) + try: + os.fchmod(fd, 0o600) + with os.fdopen(fd, "wb", closefd=True) as stream: + stream.write(canonical(record)) + stream.flush() + os.fsync(stream.fileno()) + os.replace(temporary, path) + directory_fd = os.open(path.parent, os.O_RDONLY) + try: + os.fsync(directory_fd) + finally: + os.close(directory_fd) + finally: + if os.path.exists(temporary): + os.unlink(temporary) + + +def update(path: Path, **values) -> dict: + lock_path = path.with_suffix(path.suffix + ".lock") + lock_path.parent.mkdir(parents=True, exist_ok=True) + with lock_path.open("a+b") as lock: + os.chmod(lock_path, 0o600) + fcntl.flock(lock, fcntl.LOCK_EX) + current = read_verified(path) + record = next_record(current, **values) + atomic_write(path, record) + return record + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("path", type=Path) + parser.add_argument("--expected-generation", type=int, required=True) + parser.add_argument("--operation-id", required=True) + parser.add_argument("--profile", choices=("cpu-only", "nvidia-cu128"), required=True) + parser.add_argument("--manifest-sha256", required=True) + parser.add_argument("--state", choices=tuple(state for state in TRANSITIONS if state), required=True) + parser.add_argument("--checkpoint-id") + args = parser.parse_args() + try: + record = update( + args.path, expected_generation=args.expected_generation, + operation_id=args.operation_id, profile=args.profile, + manifest_sha256=args.manifest_sha256, state=args.state, + checkpoint_id=args.checkpoint_id, + ) + except (JournalError, json.JSONDecodeError) as error: + print(json.dumps({"written": False, "code": str(error)}, sort_keys=True)) + return 1 + print(json.dumps({"written": True, **record}, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/autoinstall/offline/build-bundle.sh b/autoinstall/offline/build-bundle.sh index be24098..266eb44 100755 --- a/autoinstall/offline/build-bundle.sh +++ b/autoinstall/offline/build-bundle.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Stage all network-fetched Server Starter inputs for a disconnected install. +# Stage reviewed, immutable Offline Workstation inputs for a disconnected install. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -7,10 +7,20 @@ PACKAGE_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" OUTPUT_DIR="${OFFLINE_OUTPUT_DIR:-$PACKAGE_DIR/autoinstall/offline-output}" PAYLOAD_DIR="$OUTPUT_DIR/payload" MANIFEST_DIR="$OUTPUT_DIR/manifest" +PAYLOAD_LOCK="${OFFLINE_PAYLOAD_LOCK:-$SCRIPT_DIR/payload-lock.json}" # shellcheck source=payload.env source "$SCRIPT_DIR/payload.env" +# Gate 2 preflight MUST run before privilege checks, network access, output deletion, or acquisition. +python3 "$SCRIPT_DIR/validate-payload-lock.py" "$PAYLOAD_LOCK" + +# The legacy acquisition path below does not yet consume the reviewed lock. Keep it unreachable until +# each acquisition step verifies bytes against the lock before placing them in trusted staging. +echo "ERROR: immutable lock preflight passed, but lock-driven acquisition is not implemented yet." >&2 +echo "ERROR: refusing to execute the legacy mutable bundle builder." >&2 +exit 1 + if [ "${EUID:-$(id -u)}" -ne 0 ]; then echo "ERROR: run as root on a connected Ubuntu 22.04 staging host." >&2 exit 1 diff --git a/autoinstall/offline/container-sources.json b/autoinstall/offline/container-sources.json new file mode 100644 index 0000000..4555a9a --- /dev/null +++ b/autoinstall/offline/container-sources.json @@ -0,0 +1,8 @@ +[ + {"id":"open-webui","version":"resolve-for-gate2","source":"ghcr.io/open-webui/open-webui:main","license":"REVIEW-REQUIRED"}, + {"id":"vllm","version":"0.10.2","source":"vllm/vllm-openai:v0.10.2","license":"Apache-2.0"}, + {"id":"dcgm-exporter","version":"3.3.0-3.2.0-ubuntu22.04","source":"nvcr.io/nvidia/k8s/dcgm-exporter:3.3.0-3.2.0-ubuntu22.04","license":"REVIEW-REQUIRED"}, + {"id":"prometheus","version":"resolve-for-gate2","source":"prom/prometheus:latest","license":"Apache-2.0"}, + {"id":"node-exporter","version":"resolve-for-gate2","source":"prom/node-exporter:latest","license":"Apache-2.0"}, + {"id":"grafana","version":"resolve-for-gate2","source":"grafana/grafana:latest","license":"AGPL-3.0-only"} +] diff --git a/autoinstall/offline/install-python-profile.py b/autoinstall/offline/install-python-profile.py new file mode 100644 index 0000000..1ab4ff8 --- /dev/null +++ b/autoinstall/offline/install-python-profile.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +"""Install a prepared Python profile offline into an isolated target with rollback.""" + +from __future__ import annotations + +import argparse +import importlib.util +import os +import shutil +import stat +import subprocess +import tempfile +from pathlib import Path + + +HERE = Path(__file__).resolve().parent + + +def load_module(name: str, filename: str): + spec = importlib.util.spec_from_file_location(name, HERE / filename) + module = importlib.util.module_from_spec(spec) + assert spec.loader + spec.loader.exec_module(module) + return module + + +journal_api = load_module("install_profile_journal", "atomic-profile-journal.py") +checkpoint_api = load_module("install_profile_checkpoint", "python-environment-checkpoint.py") + + +def fault(point: str) -> None: + if os.environ.get("NEXUS_ALLOW_TEST_FAULTS") == "1" \ + and os.environ.get("NEXUS_TEST_CRASH_AT") == point: + os._exit(97) + + +def filtered_requirements(source: Path, profile: str, destination: Path) -> None: + excluded = {"chromadb"} + if profile == "cpu-only": + excluded.add("bitsandbytes") + lines = [] + for raw in source.read_text().splitlines(): + stripped = raw.split("#", 1)[0].strip() + name = stripped.split("=", 1)[0].strip().lower().replace("_", "-") + if stripped and name not in excluded: + lines.append(stripped) + destination.write_text("\n".join(lines) + "\n") + + +def health(python: Path, profile: str) -> None: + base = "import torch,torchaudio,torchvision,transformers,peft,accelerate,sentence_transformers;" + if profile == "cpu-only": + check = (base + "assert torch.__version__=='2.8.0+cpu';assert torch.version.cuda is None;" + "assert not torch.cuda.is_available();assert (torch.tensor([2.0])*2).item()==4.0") + else: + check = (base + "import bitsandbytes;assert torch.__version__.startswith('2.8.0');" + "assert torch.version.cuda=='12.8';assert torch.cuda.is_available();" + "x=torch.tensor([2.0],device='cuda');assert (x*2).item()==4.0;torch.cuda.synchronize()") + subprocess.run([str(python), "-c", check], check=True) + + +def validate_service_probe(probe: Path | None) -> None: + if probe is None: + return + if not probe.is_absolute(): + raise ValueError("SERVICE_HEALTH_PROBE_NOT_ABSOLUTE") + if probe.is_symlink() or not probe.is_file(): + raise ValueError("SERVICE_HEALTH_PROBE_NOT_REGULAR") + if not stat.S_IMODE(probe.stat().st_mode) & 0o111: + raise ValueError("SERVICE_HEALTH_PROBE_NOT_EXECUTABLE") + + +def service_health(probe: Path | None, target: Path, profile: str, timeout_seconds: int) -> None: + validate_service_probe(probe) + if probe is not None: + subprocess.run([str(probe), str(target), profile], check=True, + timeout=timeout_seconds) + + +def install(journal_path: Path, checkpoint: Path, target: Path, wheel_dir: Path, + requirements: Path, python_executable: Path, service_health_probe: Path | None = None, + service_health_timeout_seconds: int = 300) -> dict: + if service_health_timeout_seconds < 1 or service_health_timeout_seconds > 3600: + raise ValueError("SERVICE_HEALTH_TIMEOUT_INVALID") + validate_service_probe(service_health_probe) + current = journal_api.read_verified(journal_path) + if not current or current["state"] != "checkpoint_ready": + raise ValueError("JOURNAL_NOT_CHECKPOINT_READY") + checkpoint_api.verify(checkpoint, target) + identity = dict(operation_id=current["operation_id"], profile=current["selected_profile"], + manifest_sha256=current["manifest_sha256"]) + current = journal_api.update(journal_path, expected_generation=current["generation"], + state="installing", checkpoint_id=current["checkpoint_id"], **identity) + temporary = Path(tempfile.mkdtemp(prefix=f".{target.name}.install.", dir=target.parent)) + shutil.rmtree(temporary) + activated = False + req: Path | None = None + try: + subprocess.run([str(python_executable), "-m", "venv", str(temporary)], check=True) + req = temporary.parent / f".{temporary.name}.requirements.txt" + filtered_requirements(requirements, identity["profile"], req) + subprocess.run([str(temporary / "bin/pip"), "install", "--no-index", + f"--find-links={wheel_dir}", "-r", str(req)], check=True) + req.unlink() + health(temporary / "bin/python", identity["profile"]) + checkpoint_api.fsync_tree(temporary) + previous = target.with_name(f".{target.name}.install-previous") + if previous.exists(): + raise ValueError("INSTALL_PREVIOUS_EXISTS") + if target.exists(): + os.replace(target, previous) + os.replace(temporary, target) + activated = True + directory_fd = os.open(target.parent, os.O_RDONLY) + try: + os.fsync(directory_fd) + finally: + os.close(directory_fd) + fault("after_activation") + health(target / "bin/python", identity["profile"]) + service_health(service_health_probe, target, identity["profile"], + service_health_timeout_seconds) + current = journal_api.update(journal_path, expected_generation=current["generation"], + state="installed", checkpoint_id=current["checkpoint_id"], **identity) + return {"installed": True, "generation": current["generation"], + "selected_profile": identity["profile"]} + except Exception: + current = journal_api.update(journal_path, expected_generation=current["generation"], + state="rollback_required", checkpoint_id=current["checkpoint_id"], + **identity) + if activated: + checkpoint_api.restore(checkpoint, target) + else: + checkpoint_api.verify(checkpoint, target) + journal_api.update(journal_path, expected_generation=current["generation"], state="rolled_back", + checkpoint_id=current["checkpoint_id"], **identity) + raise + finally: + if temporary.exists(): + shutil.rmtree(temporary) + if req is not None and req.exists(): + req.unlink() + + +def recover(journal_path: Path, checkpoint: Path, target: Path) -> dict: + current = journal_api.read_verified(journal_path) + if not current or current["state"] not in {"installing", "rollback_required"}: + raise ValueError("JOURNAL_NOT_RECOVERABLE") + identity = dict(operation_id=current["operation_id"], profile=current["selected_profile"], + manifest_sha256=current["manifest_sha256"]) + if current["state"] == "installing": + current = journal_api.update(journal_path, expected_generation=current["generation"], + state="rollback_required", checkpoint_id=current["checkpoint_id"], + **identity) + manifest = checkpoint_api.verify(checkpoint, target) + if target.exists() and checkpoint_api.inventory(target) != manifest["entries"]: + checkpoint_api.restore(checkpoint, target) + else: + checkpoint_api.recover_restore(checkpoint, target) + current = journal_api.update(journal_path, expected_generation=current["generation"], + state="rolled_back", checkpoint_id=current["checkpoint_id"], **identity) + return {"recovered": True, "generation": current["generation"], "state": current["state"]} + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--journal", type=Path, required=True) + parser.add_argument("--checkpoint", type=Path, required=True) + parser.add_argument("--target", type=Path, required=True) + parser.add_argument("--wheel-dir", type=Path, required=True) + parser.add_argument("--requirements", type=Path, required=True) + parser.add_argument("--python", type=Path, required=True) + parser.add_argument("--service-health-probe", type=Path) + parser.add_argument("--service-health-timeout-seconds", type=int, default=300) + parser.add_argument("--recover-only", action="store_true") + args = parser.parse_args() + if args.recover_only: + result = recover(args.journal, args.checkpoint, args.target) + else: + result = install(args.journal, args.checkpoint, args.target, args.wheel_dir, + args.requirements, args.python, args.service_health_probe, + args.service_health_timeout_seconds) + print(result) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/autoinstall/offline/payload-lock.candidate.json b/autoinstall/offline/payload-lock.candidate.json new file mode 100644 index 0000000..d6390b1 --- /dev/null +++ b/autoinstall/offline/payload-lock.candidate.json @@ -0,0 +1,26 @@ +{ + "schema": "nexus-one-ai.offline-workstation-payload-lock.v1", + "product": "offline-workstation", + "architecture": "amd64", + "ubuntu_release": "22.04.5", + "approval_record": "CT-G1-2026-009", + "status": "candidate", + "unresolved": [ + "ubuntu ISO bytes and digest", + "exact APT dependency closure", + "hash-locked Python dependency closure", + "Miniconda or replacement runtime archive", + "Ollama runtime archive", + "OCI image manifests and layers by digest", + "approved model files, notices, bytes and digests", + "source snapshot archive bytes and digest" + ], + "objects": { + "apt": [], + "python": [], + "runtime": [], + "containers": [], + "models": [], + "source": [] + } +} diff --git a/autoinstall/offline/payload.env b/autoinstall/offline/payload.env index a93dc44..7dd3260 100644 --- a/autoinstall/offline/payload.env +++ b/autoinstall/offline/payload.env @@ -1,5 +1,5 @@ -# Offline Server Starter release inputs. Mutable tags are resolved to immutable -# image digests in the generated provenance file at bundle-build time. +# Legacy Offline Workstation inputs retained only for migration into payload-lock.json. +# build-bundle.sh refuses to acquire from these mutable values. OFFLINE_SCHEMA="cezen.offline_payload.v1" OFFLINE_TIER="workstation" UBUNTU_RELEASE="22.04.5" diff --git a/autoinstall/offline/plan-python-runtime-profile.py b/autoinstall/offline/plan-python-runtime-profile.py new file mode 100644 index 0000000..37f594e --- /dev/null +++ b/autoinstall/offline/plan-python-runtime-profile.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""Plan fail-closed Python runtime profile selection without mutating the host.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + + +class PlanError(ValueError): + def __init__(self, code: str, message: str): + super().__init__(message) + self.code = code + + +def choose_profile(probe: dict, requested: str) -> str: + if probe.get("architecture") != "amd64": + raise PlanError("ARCHITECTURE_UNSUPPORTED", "Offline Workstation profile requires AMD64") + nvidia = probe.get("nvidia", {}) + ready = all(nvidia.get(key) is True for key in ("present", "supported", "driver_ready")) + if requested == "cpu-only": + return "cpu-only" + if requested == "nvidia-cu128": + if not ready: + raise PlanError("NVIDIA_PROFILE_NOT_READY", "requested NVIDIA profile is not ready") + return "nvidia-cu128" + if requested != "auto": + raise PlanError("PROFILE_UNKNOWN", f"unknown requested profile: {requested}") + return "nvidia-cu128" if ready else "cpu-only" + + +def plan(probe: dict, receipt: dict, requested: str, operation_id: str, journal: dict | None, + allow_test_receipt: bool = False) -> dict: + if receipt.get("schema") != "com.cezen.nexus-one.profile-membership-verification-receipt.v1": + raise PlanError("MEMBERSHIP_RECEIPT_INVALID", "membership receipt schema is invalid") + if receipt.get("result") != "verified": + raise PlanError("MEMBERSHIP_UNVERIFIED", "signed profile membership is not verified") + if receipt.get("environment") != "production" and not allow_test_receipt: + raise PlanError("TEST_RECEIPT_FORBIDDEN", "test receipt is forbidden for production planning") + if receipt.get("namespace") != "offline-workstation/python-runtime-profiles": + raise PlanError("MEMBERSHIP_RECEIPT_INVALID", "membership namespace is invalid") + if not receipt.get("manifest_sha256"): + raise PlanError("MEMBERSHIP_UNVERIFIED", "verified membership lacks manifest identity") + selected = choose_profile(probe, requested) + if selected not in receipt.get("profiles", []): + raise PlanError("PROFILE_NOT_IN_SIGNED_MEMBERSHIP", f"{selected} is not signed membership") + + if journal: + state = journal.get("state") + previous = journal.get("selected_profile") + if state != "rolled_back": + if journal.get("operation_id") != operation_id: + raise PlanError("ACTIVE_OPERATION_EXISTS", "another profile operation is active") + if previous != selected: + raise PlanError( + "PROFILE_CHANGE_REQUIRES_ROLLBACK", + f"journal selected {previous}; requested selection is {selected}", + ) + return { + "action": "resume", + "operation_id": operation_id, + "selected_profile": selected, + "resume_from": state, + "manifest_sha256": receipt["manifest_sha256"], + "mutation_allowed_after_journal_write": True, + } + if journal.get("operation_id") == operation_id: + raise PlanError("NEW_OPERATION_REQUIRED", "rolled-back selection requires a new operation id") + + return { + "action": "start", + "operation_id": operation_id, + "selected_profile": selected, + "journal_state": "profile_selected", + "checkpoint_required": True, + "manifest_sha256": receipt["manifest_sha256"], + "mutation_allowed_after_journal_write": True, + } + + +def load(path: Path | None) -> dict | None: + return json.loads(path.read_text(encoding="utf-8")) if path else None + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--probe", type=Path, required=True) + parser.add_argument("--membership-receipt", type=Path, required=True) + parser.add_argument("--requested", choices=("auto", "cpu-only", "nvidia-cu128"), default="auto") + parser.add_argument("--operation-id", required=True) + parser.add_argument("--journal", type=Path) + parser.add_argument("--allow-test-receipt", action="store_true") + args = parser.parse_args() + try: + result = plan(load(args.probe), load(args.membership_receipt), args.requested, + args.operation_id, load(args.journal), args.allow_test_receipt) + except PlanError as error: + print(json.dumps({"allowed": False, "code": error.code, "message": str(error)}, sort_keys=True)) + return 1 + print(json.dumps({"allowed": True, **result}, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/autoinstall/offline/prepare-python-profile-operation.py b/autoinstall/offline/prepare-python-profile-operation.py new file mode 100644 index 0000000..fe483aa --- /dev/null +++ b/autoinstall/offline/prepare-python-profile-operation.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""Orchestrate verified profile selection through durable checkpoint readiness only.""" + +from __future__ import annotations + +import argparse +import importlib.util +import json +from pathlib import Path + + +HERE = Path(__file__).resolve().parent + + +def load_module(name: str, filename: str): + spec = importlib.util.spec_from_file_location(name, HERE / filename) + module = importlib.util.module_from_spec(spec) + assert spec.loader + spec.loader.exec_module(module) + return module + + +planner = load_module("profile_planner", "plan-python-runtime-profile.py") +journal_api = load_module("profile_journal", "atomic-profile-journal.py") +checkpoint_api = load_module("profile_checkpoint", "python-environment-checkpoint.py") +profile_api = load_module("profile_validator", "validate-python-runtime-profile.py") + + +def prepare(probe: dict, receipt: dict, requested: str, operation_id: str, journal_path: Path, + target: Path, checkpoint: Path, contract: dict, wheel_dir: Path, + allow_test_receipt: bool = False) -> dict: + current = journal_api.read_verified(journal_path) + planned = planner.plan(probe, receipt, requested, operation_id, current, allow_test_receipt) + selected = planned["selected_profile"] + errors = profile_api.validate(contract, selected, wheel_dir) + if errors: + raise ValueError("PROFILE_PAYLOAD_INVALID:" + "|".join(errors)) + + if current is None: + current = journal_api.update( + journal_path, expected_generation=0, operation_id=operation_id, profile=selected, + manifest_sha256=receipt["manifest_sha256"], state="profile_selected", checkpoint_id=None, + ) + if current["state"] == "profile_selected": + if checkpoint.exists(): + checkpoint_manifest = checkpoint_api.verify(checkpoint, target) + else: + checkpoint_manifest = checkpoint_api.create(target, checkpoint, operation_id) + current = journal_api.update( + journal_path, expected_generation=current["generation"], operation_id=operation_id, + profile=selected, manifest_sha256=receipt["manifest_sha256"], state="checkpoint_ready", + checkpoint_id=checkpoint_manifest["inventory_sha256"], + ) + if current["state"] != "checkpoint_ready": + raise ValueError(f"OPERATION_NOT_PREPARABLE:{current['state']}") + return {"ready": True, "selected_profile": selected, "generation": current["generation"], + "checkpoint_id": current["checkpoint_id"], "mutation_performed": False} + + +def read(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--probe", type=Path, required=True) + parser.add_argument("--receipt", type=Path, required=True) + parser.add_argument("--requested", choices=("auto", "cpu-only", "nvidia-cu128"), default="auto") + parser.add_argument("--operation-id", required=True) + parser.add_argument("--journal", type=Path, required=True) + parser.add_argument("--target", type=Path, required=True) + parser.add_argument("--checkpoint", type=Path, required=True) + parser.add_argument("--profile-contract", type=Path, required=True) + parser.add_argument("--wheel-dir", type=Path, required=True) + parser.add_argument("--allow-test-receipt", action="store_true") + args = parser.parse_args() + try: + result = prepare( + read(args.probe), read(args.receipt), args.requested, args.operation_id, args.journal, + args.target, args.checkpoint, read(args.profile_contract), args.wheel_dir, + args.allow_test_receipt, + ) + except Exception as error: + print(json.dumps({"ready": False, "code": str(error)}, sort_keys=True)) + return 1 + print(json.dumps(result, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/autoinstall/offline/python-environment-checkpoint.py b/autoinstall/offline/python-environment-checkpoint.py new file mode 100644 index 0000000..93180a9 --- /dev/null +++ b/autoinstall/offline/python-environment-checkpoint.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +"""Create, verify and restore a hashed Python-environment directory checkpoint.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import shutil +import tempfile +from pathlib import Path + + +class CheckpointError(ValueError): + pass + + +def fault(point: str) -> None: + if os.environ.get("NEXUS_ALLOW_TEST_FAULTS") == "1" \ + and os.environ.get("NEXUS_TEST_CRASH_AT") == point: + os._exit(97) + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def inventory(root: Path) -> list[dict]: + result = [] + if not root.exists(): + return result + for path in sorted(root.rglob("*")): + relative = path.relative_to(root).as_posix() + mode = path.lstat().st_mode & 0o7777 + if path.is_symlink(): + result.append({"path": relative, "type": "symlink", "mode": mode, + "target": os.readlink(path)}) + elif path.is_file(): + result.append({"path": relative, "type": "file", "mode": mode, + "bytes": path.stat().st_size, "sha256": sha256(path)}) + elif path.is_dir(): + result.append({"path": relative, "type": "directory", "mode": mode}) + else: + raise CheckpointError(f"UNSUPPORTED_ENTRY:{relative}") + return result + + +def canonical(value: dict) -> bytes: + return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode() + + +def fsync_tree(root: Path) -> None: + for path in root.rglob("*"): + if path.is_file() and not path.is_symlink(): + fd = os.open(path, os.O_RDONLY) + try: + os.fsync(fd) + finally: + os.close(fd) + for path in sorted((p for p in root.rglob("*") if p.is_dir()), reverse=True): + fd = os.open(path, os.O_RDONLY) + try: + os.fsync(fd) + finally: + os.close(fd) + fd = os.open(root, os.O_RDONLY) + try: + os.fsync(fd) + finally: + os.close(fd) + + +def create(target: Path, checkpoint: Path, operation_id: str) -> dict: + if checkpoint.exists(): + raise CheckpointError("CHECKPOINT_ALREADY_EXISTS") + checkpoint.parent.mkdir(parents=True, exist_ok=True) + temporary = Path(tempfile.mkdtemp(prefix=f".{checkpoint.name}.", dir=checkpoint.parent)) + try: + payload = temporary / "payload" + existed = target.exists() + if existed: + if not target.is_dir() or target.is_symlink(): + raise CheckpointError("TARGET_NOT_DIRECTORY") + shutil.copytree(target, payload, symlinks=True) + manifest = { + "schema": "com.cezen.nexus-one.python-environment-checkpoint.v1", + "operation_id": operation_id, + "target": str(target), + "target_existed": existed, + "entries": inventory(payload), + } + manifest["inventory_sha256"] = hashlib.sha256(canonical(manifest)).hexdigest() + (temporary / "manifest.json").write_bytes(canonical(manifest)) + fsync_tree(temporary) + fault("create_before_replace") + os.replace(temporary, checkpoint) + fault("create_after_replace") + directory_fd = os.open(checkpoint.parent, os.O_RDONLY) + try: + os.fsync(directory_fd) + finally: + os.close(directory_fd) + return manifest + finally: + if temporary.exists(): + shutil.rmtree(temporary) + + +def verify(checkpoint: Path, expected_target: Path | None = None) -> dict: + manifest = json.loads((checkpoint / "manifest.json").read_text()) + recorded = manifest.pop("inventory_sha256", None) + if recorded != hashlib.sha256(canonical(manifest)).hexdigest(): + raise CheckpointError("CHECKPOINT_MANIFEST_INVALID") + manifest["inventory_sha256"] = recorded + if expected_target is not None and manifest["target"] != str(expected_target): + raise CheckpointError("CHECKPOINT_TARGET_MISMATCH") + actual = inventory(checkpoint / "payload") + if actual != manifest["entries"]: + raise CheckpointError("CHECKPOINT_PAYLOAD_INVALID") + return manifest + + +def restore(checkpoint: Path, target: Path) -> dict: + manifest = verify(checkpoint, target) + target.parent.mkdir(parents=True, exist_ok=True) + if not manifest["target_existed"]: + if target.exists(): + quarantine = target.with_name(f".{target.name}.rollback-discard") + if quarantine.exists(): + raise CheckpointError("ROLLBACK_QUARANTINE_EXISTS") + os.replace(target, quarantine) + fault("restore_after_quarantine") + directory_fd = os.open(target.parent, os.O_RDONLY) + try: + os.fsync(directory_fd) + finally: + os.close(directory_fd) + return manifest + temporary = Path(tempfile.mkdtemp(prefix=f".{target.name}.restore.", dir=target.parent)) + shutil.rmtree(temporary) + shutil.copytree(checkpoint / "payload", temporary, symlinks=True) + fsync_tree(temporary) + if target.exists(): + quarantine = target.with_name(f".{target.name}.rollback-discard") + if quarantine.exists(): + raise CheckpointError("ROLLBACK_QUARANTINE_EXISTS") + os.replace(target, quarantine) + fault("restore_after_quarantine") + os.replace(temporary, target) + fault("restore_after_replace") + directory_fd = os.open(target.parent, os.O_RDONLY) + try: + os.fsync(directory_fd) + finally: + os.close(directory_fd) + return manifest + + +def recover_restore(checkpoint: Path, target: Path) -> dict: + manifest = verify(checkpoint, target) + quarantine = target.with_name(f".{target.name}.rollback-discard") + if target.exists(): + if manifest["target_existed"] and inventory(target) == manifest["entries"]: + return manifest + if not manifest["target_existed"]: + raise CheckpointError("ROLLBACK_TARGET_SHOULD_BE_ABSENT") + raise CheckpointError("ROLLBACK_TARGET_AMBIGUOUS") + if not manifest["target_existed"]: + return manifest + if not quarantine.exists(): + raise CheckpointError("ROLLBACK_SOURCE_STATE_MISSING") + return restore(checkpoint, target) + + +def main() -> int: + parser = argparse.ArgumentParser() + sub = parser.add_subparsers(dest="command", required=True) + create_parser = sub.add_parser("create") + create_parser.add_argument("target", type=Path) + create_parser.add_argument("checkpoint", type=Path) + create_parser.add_argument("--operation-id", required=True) + verify_parser = sub.add_parser("verify") + verify_parser.add_argument("checkpoint", type=Path) + verify_parser.add_argument("--target", type=Path) + restore_parser = sub.add_parser("restore") + restore_parser.add_argument("checkpoint", type=Path) + restore_parser.add_argument("target", type=Path) + recover_parser = sub.add_parser("recover-restore") + recover_parser.add_argument("checkpoint", type=Path) + recover_parser.add_argument("target", type=Path) + args = parser.parse_args() + try: + if args.command == "create": + result = create(args.target, args.checkpoint, args.operation_id) + elif args.command == "verify": + result = verify(args.checkpoint, args.target) + elif args.command == "restore": + result = restore(args.checkpoint, args.target) + else: + result = recover_restore(args.checkpoint, args.target) + except (CheckpointError, OSError, json.JSONDecodeError) as error: + print(json.dumps({"ok": False, "code": str(error)}, sort_keys=True)) + return 1 + print(json.dumps({"ok": True, "inventory_sha256": result["inventory_sha256"], + "entries": len(result["entries"])}, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/autoinstall/offline/python-packages.txt b/autoinstall/offline/python-packages.txt index 0a0e15e..4a9e30a 100644 --- a/autoinstall/offline/python-packages.txt +++ b/autoinstall/offline/python-packages.txt @@ -10,6 +10,10 @@ langchain-community llama-index transformers huggingface-hub +# Match the approved vLLM 0.10.2 CUDA 12.8 binary baseline. +torch==2.8.0 +torchaudio==2.8.0 +torchvision==0.23.0 peft bitsandbytes accelerate diff --git a/autoinstall/offline/python-runtime-profiles.json b/autoinstall/offline/python-runtime-profiles.json new file mode 100644 index 0000000..5c04293 --- /dev/null +++ b/autoinstall/offline/python-runtime-profiles.json @@ -0,0 +1,29 @@ +{ + "schema": "nexus-one-ai.python-runtime-profiles.v1", + "namespace": "offline-workstation/python-runtime-profiles", + "architecture": "amd64", + "python": "3.11", + "selection": "exactly-one", + "profiles": { + "cpu-only": { + "index": "https://download.pytorch.org/whl/cpu", + "required": { + "torch-2.8.0+cpu-cp311-cp311-manylinux_2_28_x86_64.whl": "cb06175284673a581dd91fb1965662ae4ecaba6e5c357aa0ea7bb8b84b6b7eeb", + "torchaudio-2.8.0+cpu-cp311-cp311-manylinux_2_28_x86_64.whl": "e54bd7fc9472019308097d99102df9acee22aa2451ae808d27840bc874320292", + "torchvision-0.23.0+cpu-cp311-cp311-manylinux_2_28_x86_64.whl": "d83d8075db43b8ca89680bdeb2f100c832e2a3aa61ee42c038b1a146e5e511b6" + }, + "forbidden_filename_patterns": [ + "^nvidia_", "cu12", "cu13", "^triton-", "^bitsandbytes-" + ] + }, + "nvidia-cu128": { + "index": "https://download.pytorch.org/whl/cu128", + "required": { + "torch-2.8.0-cp311-cp311-manylinux_2_28_x86_64.whl": "c12fa219f51a933d5f80eeb3a7a5d0cbe9168c0a14bbb4055f1979431660879b", + "torchaudio-2.8.0-cp311-cp311-manylinux_2_28_x86_64.whl": "776c0b4ba84b9e3ddf6304b9c47cd63549d7896a6f3d5184ece074cc3d76ed6b", + "torchvision-0.23.0-cp311-cp311-manylinux_2_28_x86_64.whl": "35c27941831b653f5101edfe62c03d196c13f32139310519e8228f35eae0e96a" + }, + "forbidden_filename_patterns": ["cu13"] + } + } +} diff --git a/autoinstall/offline/recover-python-profile-on-boot.py b/autoinstall/offline/recover-python-profile-on-boot.py new file mode 100644 index 0000000..5cca2c5 --- /dev/null +++ b/autoinstall/offline/recover-python-profile-on-boot.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Recover interrupted Python profile mutation safely after process/reboot restart.""" + +from __future__ import annotations + +import argparse +import importlib.util +import json +from pathlib import Path + + +HERE = Path(__file__).resolve().parent + + +def load_module(name: str, filename: str): + spec = importlib.util.spec_from_file_location(name, HERE / filename) + module = importlib.util.module_from_spec(spec) + assert spec.loader + spec.loader.exec_module(module) + return module + + +installer = load_module("boot_profile_installer", "install-python-profile.py") +journal_api = load_module("boot_profile_journal", "atomic-profile-journal.py") + + +def recover(journal: Path, checkpoint: Path, target: Path) -> dict: + current = journal_api.read_verified(journal) + if current is None: + return {"action": "none", "reason": "journal_absent"} + state = current["state"] + if state in {"installing", "rollback_required"}: + result = installer.recover(journal, checkpoint, target) + return {"action": "rollback_recovered", **result} + if state in {"installed", "rolled_back"}: + return {"action": "none", "reason": f"terminal_{state}"} + if state in {"profile_selected", "checkpoint_ready"}: + return {"action": "none", "reason": f"non_mutating_{state}"} + raise ValueError(f"UNKNOWN_JOURNAL_STATE:{state}") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--journal", type=Path, required=True) + parser.add_argument("--checkpoint", type=Path, required=True) + parser.add_argument("--target", type=Path, required=True) + args = parser.parse_args() + try: + result = recover(args.journal, args.checkpoint, args.target) + except Exception as error: + print(json.dumps({"recovered": False, "code": str(error)}, sort_keys=True)) + return 1 + print(json.dumps(result, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/autoinstall/offline/resolve-apt-lock.py b/autoinstall/offline/resolve-apt-lock.py new file mode 100644 index 0000000..ead515f --- /dev/null +++ b/autoinstall/offline/resolve-apt-lock.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""Resolve an Ubuntu APT dependency closure into candidate payload-lock objects.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import subprocess +from pathlib import Path + + +def filtered_lines(path: Path) -> list[str]: + return [ + line.strip() for line in path.read_text(encoding="utf-8").splitlines() + if line.strip() and not line.lstrip().startswith("#") + ] + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def deb_field(path: Path, field: str) -> str: + result = subprocess.run( + ["dpkg-deb", "-f", str(path), field], check=True, capture_output=True, text=True + ) + return result.stdout.strip() + + +def resolve(packages: list[str], work: Path) -> list[dict]: + archives = work / "archives" + archives.mkdir(parents=True, exist_ok=True) + (archives / "partial").mkdir(exist_ok=True) + status = work / "empty-dpkg-status" + status.write_text("", encoding="utf-8") + command = [ + "apt-get", "--download-only", "--yes", "--no-install-recommends", + "-o", "Debug::NoLocking=1", + "-o", f"Dir::State::status={status}", + "-o", f"Dir::Cache::archives={archives}", + "-o", "APT::Get::AllowUnauthenticated=false", + "install", *packages, + ] + subprocess.run(command, check=True) + debs = sorted(archives.glob("*.deb")) + if not debs: + raise RuntimeError("APT resolution produced no .deb files") + objects = [] + for path in debs: + package = deb_field(path, "Package") + version = deb_field(path, "Version") + architecture = deb_field(path, "Architecture") + if architecture not in {"amd64", "all"}: + raise RuntimeError(f"unexpected architecture {architecture} for {path.name}") + digest = sha256_file(path) + objects.append({ + "id": f"apt-{package}", + "version": version, + "architecture": architecture, + "source": f"apt://ubuntu-22.04/{package}/{version}/{path.name}", + "target": f"payload/apt/{path.name}", + "sha256": digest, + "bytes": path.stat().st_size, + "bytes_basis": "measured", + "license": "REVIEW-REQUIRED", + "requested_directly": package in packages, + }) + return objects + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--lock", type=Path, required=True) + parser.add_argument("--packages", type=Path, required=True) + parser.add_argument("--work", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + + lock = json.loads(args.lock.read_text(encoding="utf-8")) + packages = filtered_lines(args.packages) + objects = resolve(packages, args.work) + lock["objects"]["apt"] = objects + lock["unresolved"] = [x for x in lock["unresolved"] if x != "exact APT dependency closure"] + lock["unresolved"].extend([ + "APT repository snapshot/index digests and origin approval", + "APT package license mapping and notice extraction", + "approved R570/CUDA 12.8.1/cuDNN/NVIDIA Container Toolkit package closure" + ]) + lock["unresolved"] = list(dict.fromkeys(lock["unresolved"])) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(lock, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(f"APT requested packages: {len(packages)}") + print(f"APT resolved .deb objects: {len(objects)}") + print(f"APT measured bytes: {sum(item['bytes'] for item in objects)}") + print(f"Wrote candidate lock: {args.output}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/autoinstall/offline/resolve-payload-lock.py b/autoinstall/offline/resolve-payload-lock.py new file mode 100644 index 0000000..5998de4 --- /dev/null +++ b/autoinstall/offline/resolve-payload-lock.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +"""Resolve source snapshot and AMD64 OCI metadata into a Gate 2 candidate lock.""" + +from __future__ import annotations + +import argparse +import gzip +import hashlib +import io +import json +import os +import subprocess +import tarfile +from pathlib import Path + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def deterministic_source_archive(source: Path, output: Path) -> dict: + output.parent.mkdir(parents=True, exist_ok=True) + excluded_parts = {".git", "__pycache__", ".pytest_cache", "node_modules", ".venv", "venv"} + files = sorted( + path for path in source.rglob("*") + if path.is_file() + and not excluded_parts.intersection(path.relative_to(source).parts) + and path.suffix != ".iso" + and path.name != "how-it-flows.gif" + ) + with output.open("wb") as raw: + with gzip.GzipFile(filename="", mode="wb", fileobj=raw, mtime=0) as compressed: + with tarfile.open(fileobj=compressed, mode="w") as archive: + for path in files: + relative = path.relative_to(source) + info = archive.gettarinfo(str(path), arcname=str(Path("aipackage") / relative)) + info.uid = info.gid = 0 + info.uname = info.gname = "root" + info.mtime = 0 + with path.open("rb") as stream: + archive.addfile(info, stream) + return { + "id": "nexus-one-ai-source-snapshot", + "version": os.environ.get("OFFLINE_SOURCE_COMMIT", "exported-snapshot"), + "architecture": "all", + "source": "local-reviewed-source-snapshot", + "target": "payload/source/aipackage.tar.gz", + "sha256": sha256_file(output), + "bytes": output.stat().st_size, + "bytes_basis": "measured", + "license": "Proprietary-Cezen-Technologies", + "file_count": len(files), + } + + +def manifest_records(reference: str) -> list[dict]: + result = subprocess.run( + ["docker", "manifest", "inspect", "--verbose", reference], + check=True, + capture_output=True, + text=True, + ) + parsed = json.loads(result.stdout) + return parsed if isinstance(parsed, list) else [parsed] + + +def resolve_container(item: dict) -> dict: + records = manifest_records(item["source"]) + matches = [ + record for record in records + if record.get("Descriptor", {}).get("platform", {}).get("os") == "linux" + and record.get("Descriptor", {}).get("platform", {}).get("architecture") == "amd64" + ] + if len(matches) != 1: + raise RuntimeError(f"{item['source']}: expected one linux/amd64 manifest, found {len(matches)}") + record = matches[0] + descriptor = record["Descriptor"] + digest = descriptor["digest"] + manifest = record.get("SchemaV2Manifest") or {} + content = [manifest.get("config", {})] + list(manifest.get("layers", [])) + declared_bytes = sum(entry.get("size", 0) for entry in content) + return { + "id": item["id"], + "version": item["version"], + "architecture": "amd64", + "source": f"{item['source'].split(':')[0]}@{digest}", + "resolved_from": item["source"], + "target": f"payload/containers/{item['id']}.oci.tar", + "sha256": digest.removeprefix("sha256:"), + "bytes": declared_bytes, + "bytes_basis": "registry-declared-content", + "license": item["license"], + "manifest_bytes": descriptor.get("size"), + "config": manifest.get("config"), + "layers": manifest.get("layers", []), + } + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--candidate", type=Path, required=True) + parser.add_argument("--source-root", type=Path, required=True) + parser.add_argument("--container-sources", type=Path, required=True) + parser.add_argument("--staging", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + + lock = json.loads(args.candidate.read_text(encoding="utf-8")) + source_object = deterministic_source_archive( + args.source_root.resolve(), args.staging / "source" / "aipackage.tar.gz" + ) + declarations = json.loads(args.container_sources.read_text(encoding="utf-8")) + containers = [resolve_container(item) for item in declarations] + lock["objects"]["source"] = [source_object] + lock["objects"]["containers"] = containers + lock["unresolved"] = [ + item for item in lock["unresolved"] + if item not in {"source snapshot archive bytes and digest", "OCI image manifests and layers by digest"} + ] + lock["unresolved"].extend([ + "OCI archive bytes and SHA-256 after digest-pinned acquisition", + "license review for container entries marked REVIEW-REQUIRED", + "replace mutable container source tags with approved release tags before final review" + ]) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(lock, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(f"Wrote Gate 2 candidate lock: {args.output}") + print(f"Source files: {source_object['file_count']}; source bytes: {source_object['bytes']}") + print(f"AMD64 OCI manifests resolved: {len(containers)}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/autoinstall/offline/resolve-python-lock.py b/autoinstall/offline/resolve-python-lock.py new file mode 100644 index 0000000..0eec5da --- /dev/null +++ b/autoinstall/offline/resolve-python-lock.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""Resolve one Python 3.11 wheel closure into candidate payload-lock objects.""" + +from __future__ import annotations + +import argparse +import email +import hashlib +import json +import re +import subprocess +import zipfile +from pathlib import Path + + +def requirements(path: Path) -> list[str]: + result = [] + for raw in path.read_text(encoding="utf-8").splitlines(): + line = raw.split("#", 1)[0].strip() + if line: + result.append(line) + return result + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def metadata(path: Path) -> email.message.Message: + with zipfile.ZipFile(path) as archive: + # Some wheels vendor other distributions (for example setuptools). Only the + # wheel's root dist-info directory describes the payload being locked. + names = [ + name for name in archive.namelist() + if name.endswith(".dist-info/METADATA") and name.count("/") == 1 + ] + if len(names) != 1: + raise RuntimeError(f"{path.name}: expected one dist-info/METADATA, found {len(names)}") + return email.message_from_bytes(archive.read(names[0])) + + +def normalize(name: str) -> str: + return re.sub(r"[-_.]+", "-", name).lower() + + +def resolve_group(group: str, requested: list[str], work: Path, resolver_pip: str) -> list[dict]: + group_work = work / group + wheels = group_work / "wheels" + wheels.mkdir(parents=True, exist_ok=True) + combined = group_work / "requirements.in" + combined.write_text("\n".join(dict.fromkeys(requested)) + "\n", encoding="utf-8") + + venv = work / "resolver-venv" + if not (venv / "bin/python").exists(): + subprocess.run(["python3", "-m", "venv", str(venv)], check=True) + pip = venv / "bin/pip" + subprocess.run([str(pip), "install", "--disable-pip-version-check", f"pip=={resolver_pip}"], check=True) + command = [ + str(pip), "download", "--disable-pip-version-check", "--only-binary=:all:", + "--python-version", "3.11", "--implementation", "cp", "--abi", "cp311", + "--dest", str(wheels), "--requirement", str(combined), + ] + subprocess.run(command, check=True) + + objects = [] + versions: dict[str, str] = {} + for path in sorted(wheels.glob("*.whl")): + meta = metadata(path) + name = meta.get("Name") + version = meta.get("Version") + if not name or not version: + raise RuntimeError(f"{path.name}: missing Name or Version metadata") + key = normalize(name) + previous = versions.setdefault(key, version) + if previous != version: + raise RuntimeError(f"multiple versions resolved for {name}: {previous}, {version}") + license_value = str( + meta.get("License-Expression") or meta.get("License") or "REVIEW-REQUIRED" + ) + if len(license_value) > 160 or "\n" in license_value or not license_value.strip(): + license_value = "REVIEW-REQUIRED" + architecture = "all" if path.name.endswith("-none-any.whl") else "amd64" + objects.append({ + "id": f"python-{group}-{key}", + "version": version, + "architecture": architecture, + "source": f"pypi://{key}/{version}/{path.name}", + "target": f"payload/python/{group}/{path.name}", + "sha256": sha256_file(path), + "bytes": path.stat().st_size, + "bytes_basis": "measured", + "license": license_value.strip(), + }) + if not objects: + raise RuntimeError(f"Python resolution produced no wheels for {group}") + return objects + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--lock", type=Path, required=True) + parser.add_argument("--backend-requirements", type=Path, required=True) + parser.add_argument("--platform-requirements", type=Path, required=True) + parser.add_argument("--work", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--resolver-pip", default="25.1.1") + args = parser.parse_args() + + lock = json.loads(args.lock.read_text(encoding="utf-8")) + platform_requested = requirements(args.platform_requirements) + chromadb_requested = [item for item in platform_requested if normalize(item.split("=", 1)[0]) == "chromadb"] + remaining_platform = [item for item in platform_requested if item not in chromadb_requested] + groups = { + "backend": requirements(args.backend_requirements), + "chromadb": chromadb_requested, + "platform": remaining_platform, + } + if not groups["chromadb"]: + raise RuntimeError("platform requirements do not declare the ChromaDB wheelhouse") + objects = [] + for group, requested in groups.items(): + objects.extend(resolve_group(group, requested, args.work, args.resolver_pip)) + lock["objects"]["python"] = objects + lock["unresolved"] = [x for x in lock["unresolved"] if x != "hash-locked Python dependency closure"] + lock["unresolved"].extend([ + "Python index snapshot/origin and resolver report approval", + "Python wheel license/notice review", + "Python 3.11 clean-environment import and offline installation evidence", + "GPU framework compatibility with approved CUDA 12.8.1 runtime" + ]) + lock["unresolved"] = list(dict.fromkeys(lock["unresolved"])) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(lock, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(f"Python wheel objects: {len(objects)}") + for group in groups: + print(f"Python {group} wheel objects: {sum(item['id'].startswith(f'python-{group}-') for item in objects)}") + print(f"Python measured bytes: {sum(item['bytes'] for item in objects)}") + print(f"License review required: {sum(item['license'] == 'REVIEW-REQUIRED' for item in objects)}") + print(f"Wrote candidate lock: {args.output}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/autoinstall/offline/validate-payload-lock.py b/autoinstall/offline/validate-payload-lock.py new file mode 100644 index 0000000..ab2117c --- /dev/null +++ b/autoinstall/offline/validate-payload-lock.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Fail-closed validator for the Offline Workstation Gate 2 payload lock.""" + +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path + +SHA256 = re.compile(r"^[0-9a-f]{64}$") +IMMUTABLE_IMAGE = re.compile(r"^[^\s]+@sha256:([0-9a-f]{64})$") +FORBIDDEN_SOURCE = re.compile(r"(^|[/_.:-])(latest|main|master)([/_.:@-]|$)", re.I) +REQUIRED_OBJECT_FIELDS = {"id", "version", "architecture", "source", "sha256", "bytes", "license"} +OBJECT_GROUPS = ("apt", "python", "runtime", "containers", "models", "source") + + +def fail(errors: list[str], message: str) -> None: + errors.append(message) + + +def validate(path: Path) -> list[str]: + errors: list[str] = [] + try: + data = json.loads(path.read_text(encoding="utf-8")) + except FileNotFoundError: + return [f"payload lock does not exist: {path}"] + except (OSError, json.JSONDecodeError) as exc: + return [f"payload lock is unreadable or invalid JSON: {exc}"] + + if data.get("schema") != "nexus-one-ai.offline-workstation-payload-lock.v1": + fail(errors, "schema must be nexus-one-ai.offline-workstation-payload-lock.v1") + if data.get("product") != "offline-workstation" or data.get("architecture") != "amd64": + fail(errors, "product/architecture must be offline-workstation/amd64") + if data.get("ubuntu_release") != "22.04.5": + fail(errors, "ubuntu_release must be 22.04.5") + if data.get("approval_record") != "CT-G1-2026-009": + fail(errors, "approval_record must bind the approved payload boundary CT-G1-2026-009") + if data.get("status") != "reviewed": + fail(errors, "status must be reviewed before acquisition or assembly") + if data.get("unresolved"): + fail(errors, "unresolved must be an empty array") + + objects = data.get("objects") + if not isinstance(objects, dict): + return errors + ["objects must be an object"] + + seen_ids: set[str] = set() + seen_paths: set[str] = set() + for group in OBJECT_GROUPS: + entries = objects.get(group) + if not isinstance(entries, list) or not entries: + fail(errors, f"objects.{group} must be a non-empty array") + continue + for index, entry in enumerate(entries): + label = f"objects.{group}[{index}]" + if not isinstance(entry, dict): + fail(errors, f"{label} must be an object") + continue + missing = sorted(REQUIRED_OBJECT_FIELDS - entry.keys()) + if missing: + fail(errors, f"{label} missing fields: {', '.join(missing)}") + continue + object_id = entry["id"] + if not isinstance(object_id, str) or not object_id: + fail(errors, f"{label}.id must be non-empty") + elif object_id in seen_ids: + fail(errors, f"duplicate object id: {object_id}") + else: + seen_ids.add(object_id) + if entry.get("architecture") not in {"amd64", "all"}: + fail(errors, f"{label}.architecture must be amd64 or all") + if not isinstance(entry.get("bytes"), int) or entry["bytes"] <= 0: + fail(errors, f"{label}.bytes must be a positive integer") + if data.get("status") == "reviewed" and entry.get("bytes_basis") != "measured": + fail(errors, f"{label}.bytes_basis must be measured for a reviewed lock") + if not isinstance(entry.get("sha256"), str) or not SHA256.fullmatch(entry["sha256"]): + fail(errors, f"{label}.sha256 must be 64 lowercase hexadecimal characters") + for field in ("version", "source", "license"): + value = entry.get(field) + if not isinstance(value, str) or not value.strip(): + fail(errors, f"{label}.{field} must be non-empty") + source = entry.get("source", "") + version = entry.get("version", "") + if FORBIDDEN_SOURCE.search(source) or FORBIDDEN_SOURCE.search(version): + fail(errors, f"{label} contains mutable latest/main/master identity") + target = entry.get("target") + if not isinstance(target, str) or not target or target.startswith("/") or ".." in Path(target).parts: + fail(errors, f"{label}.target must be a safe relative path") + elif target in seen_paths: + fail(errors, f"duplicate target path: {target}") + else: + seen_paths.add(target) + if group == "containers": + match = IMMUTABLE_IMAGE.fullmatch(source) + if not match: + fail(errors, f"{label}.source must be an OCI reference pinned with @sha256") + elif match.group(1) != entry["sha256"]: + fail(errors, f"{label} OCI digest must match sha256") + + return errors + + +def main() -> int: + if len(sys.argv) != 2: + print(f"usage: {Path(sys.argv[0]).name} PAYLOAD_LOCK.json", file=sys.stderr) + return 2 + errors = validate(Path(sys.argv[1])) + if errors: + for error in errors: + print(f"ERROR: {error}", file=sys.stderr) + return 1 + print("Offline Workstation payload lock preflight passed.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/autoinstall/offline/validate-python-runtime-profile.py b/autoinstall/offline/validate-python-runtime-profile.py new file mode 100644 index 0000000..d104bb6 --- /dev/null +++ b/autoinstall/offline/validate-python-runtime-profile.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Fail closed unless a selected Python wheel directory obeys its runtime profile.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +from pathlib import Path + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def validate(contract: dict, profile_name: str, wheel_dir: Path) -> list[str]: + errors: list[str] = [] + profiles = contract.get("profiles", {}) + if profile_name not in profiles: + return [f"unknown profile: {profile_name}"] + profile = profiles[profile_name] + wheels = {path.name: path for path in wheel_dir.glob("*.whl") if path.is_file()} + if not wheels: + errors.append("selected wheel directory is empty") + for pattern in profile.get("forbidden_filename_patterns", []): + matches = sorted(name for name in wheels if re.search(pattern, name, re.IGNORECASE)) + if matches: + errors.append(f"forbidden pattern {pattern!r}: {', '.join(matches)}") + for name, expected in profile.get("required", {}).items(): + path = wheels.get(name) + if path is None: + errors.append(f"missing required wheel: {name}") + elif sha256(path) != expected: + errors.append(f"digest mismatch: {name}") + return errors + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("contract", type=Path) + parser.add_argument("profile") + parser.add_argument("wheel_dir", type=Path) + args = parser.parse_args() + contract = json.loads(args.contract.read_text(encoding="utf-8")) + errors = validate(contract, args.profile, args.wheel_dir) + if errors: + for error in errors: + print(f"ERROR: {error}") + return 1 + print(f"profile valid: {args.profile}; wheels={len(list(args.wheel_dir.glob('*.whl')))}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/autoinstall/offline/verify-python-profile-membership.py b/autoinstall/offline/verify-python-profile-membership.py new file mode 100644 index 0000000..f5765fd --- /dev/null +++ b/autoinstall/offline/verify-python-profile-membership.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""Verify signed Python profile membership and emit a bound verification receipt.""" + +from __future__ import annotations + +import argparse +import base64 +import hashlib +import json +import subprocess +import tempfile +from pathlib import Path + + +class VerificationError(ValueError): + pass + + +def canonical(value: dict) -> bytes: + return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode() + + +def sha256(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def verify(manifest: dict, envelope: dict, trust: dict, public_key: Path, + environment: str, verified_at: str, verifier_build_sha256: str) -> dict: + body = canonical(manifest) + manifest_digest = sha256(body) + if manifest.get("namespace") != "offline-workstation/python-runtime-profiles": + raise VerificationError("NAMESPACE_INVALID") + if envelope.get("schema") != "com.cezen.nexus-one.signature-envelope.v1" \ + or envelope.get("algorithm") != "ed25519": + raise VerificationError("ENVELOPE_INVALID") + if envelope.get("manifest_digest") != f"sha256:{manifest_digest}": + raise VerificationError("MANIFEST_DIGEST_MISMATCH") + if envelope.get("policy_version") != trust.get("policy_version"): + raise VerificationError("POLICY_VERSION_MISMATCH") + keys = [item for item in trust.get("keys", []) if item.get("key_id") == envelope.get("key_id")] + if len(keys) != 1: + raise VerificationError("KEY_UNKNOWN_OR_AMBIGUOUS") + key = keys[0] + required = {"algorithm": "ed25519", "purpose": "profile-membership", "status": "active", + "environment": environment} + if any(key.get(name) != value for name, value in required.items()): + raise VerificationError("KEY_POLICY_REJECTED") + der = subprocess.run( + ["openssl", "pkey", "-pubin", "-in", str(public_key), "-outform", "DER"], + check=True, capture_output=True, + ).stdout + fingerprint = sha256(der) + if fingerprint != key.get("public_key_sha256"): + raise VerificationError("KEY_FINGERPRINT_MISMATCH") + try: + signature = base64.b64decode(envelope.get("signature", ""), validate=True) + except ValueError as error: + raise VerificationError("SIGNATURE_ENCODING_INVALID") from error + with tempfile.TemporaryDirectory() as directory: + body_path = Path(directory) / "manifest.canonical.json" + signature_path = Path(directory) / "signature.bin" + body_path.write_bytes(body) + signature_path.write_bytes(signature) + result = subprocess.run([ + "openssl", "pkeyutl", "-verify", "-pubin", "-inkey", str(public_key), + "-rawin", "-in", str(body_path), "-sigfile", str(signature_path), + ], capture_output=True) + if result.returncode != 0: + raise VerificationError("SIGNATURE_INVALID") + return { + "schema": "com.cezen.nexus-one.profile-membership-verification-receipt.v1", + "environment": environment, + "result": "verified", + "manifest_sha256": manifest_digest, + "envelope_sha256": sha256(canonical(envelope)), + "key_id": envelope["key_id"], + "key_fingerprint_sha256": fingerprint, + "trust_snapshot_sha256": sha256(canonical(trust)), + "policy_version": trust["policy_version"], + "namespace": manifest["namespace"], + "profiles": sorted(manifest["profiles"]), + "verified_at": verified_at, + "verifier_build_sha256": verifier_build_sha256, + } + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--manifest", type=Path, required=True) + parser.add_argument("--envelope", type=Path, required=True) + parser.add_argument("--trust-snapshot", type=Path, required=True) + parser.add_argument("--public-key", type=Path, required=True) + parser.add_argument("--environment", choices=("production", "test"), required=True) + parser.add_argument("--verified-at", required=True) + args = parser.parse_args() + script_digest = sha256(Path(__file__).read_bytes()) + try: + receipt = verify( + json.loads(args.manifest.read_text()), json.loads(args.envelope.read_text()), + json.loads(args.trust_snapshot.read_text()), args.public_key, + args.environment, args.verified_at, script_digest, + ) + except (VerificationError, subprocess.CalledProcessError, json.JSONDecodeError) as error: + print(json.dumps({"result": "rejected", "code": str(error)}, sort_keys=True)) + return 1 + print(json.dumps(receipt, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/cezen-portal/about.html b/cezen-portal/about.html index abd5b6e..ce0bca4 100644 --- a/cezen-portal/about.html +++ b/cezen-portal/about.html @@ -60,6 +60,9 @@