From 1e5c67738f69c3f0eff82126e5d411fdea26025d Mon Sep 17 00:00:00 2001 From: fwazb Date: Mon, 20 Jul 2026 16:12:01 -0700 Subject: [PATCH] Package leoclean no-send GCP staging service --- .github/workflows/ci.yml | 8 + docker/leoclean-nosend/Dockerfile | 78 ++ docker/leoclean-nosend/entrypoint.py | 295 ++++++++ docs/gcp-leoclean-nosend-runtime.md | 4 + docs/gcp-leoclean-nosend-service-package.md | 138 ++++ docs/workstreams/gcp-leoclean/goals.md | 50 ++ .../runtime/leoclean-nosend/bootstrap.py | 75 +- .../runtime/leoclean-nosend/runtime-lock.json | 2 +- ops/gcp_leoclean_nosend_package.py | 674 ++++++++++++++++++ scripts/compile_leoclean_nosend_runtime.py | 41 +- scripts/run_leoclean_nosend_runtime_canary.sh | 1 + tests/test_gcp_leoclean_nosend_package.py | 362 ++++++++++ tests/test_leoclean_nosend_runtime.py | 79 ++ 13 files changed, 1787 insertions(+), 20 deletions(-) create mode 100644 docker/leoclean-nosend/Dockerfile create mode 100644 docker/leoclean-nosend/entrypoint.py create mode 100644 docs/gcp-leoclean-nosend-service-package.md create mode 100644 docs/workstreams/gcp-leoclean/goals.md create mode 100644 ops/gcp_leoclean_nosend_package.py create mode 100644 tests/test_gcp_leoclean_nosend_package.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b232cd..dcae78a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,7 @@ jobs: lib/evaluate.py \ lib/llm.py \ lib/post_extract.py \ + docker/leoclean-nosend/entrypoint.py \ ops/apply_gcp_iam_split.py \ ops/attest_gcp_reasoning_compute.py \ ops/capture_vps_canonical_postgres_snapshot.py \ @@ -50,6 +51,7 @@ jobs: ops/apply_gcp_runtime_baseline.py \ ops/check_gcp_service_communications.py \ ops/derive_leoclean_runtime_query_contract.py \ + ops/gcp_leoclean_nosend_package.py \ ops/plan_gcp_iam_split.py \ ops/private_receipt_io.py \ ops/run_local_canonical_postgres_rebuild.py \ @@ -63,8 +65,13 @@ jobs: ops/verify_postgres_parity_manifest.py \ ops/verify_vps_canonical_snapshot_delta.py \ telegram/approvals.py \ + hermes-agent/patches/enforce_leoclean_learning_policy.py \ hermes-agent/leoclean-bin/kb_tool.py \ hermes-agent/leoclean-bin/cloudsql_memory_tool.py \ + hermes-agent/runtime/leoclean-nosend/bootstrap.py \ + hermes-agent/runtime/leoclean-nosend/profile-sources/cloudsql_memory_runtime.py \ + hermes-agent/runtime/leoclean-nosend/profile-sources/leo_db_context.py \ + hermes-agent/runtime/leoclean-nosend/profile-sources/operational_contracts.py \ scripts/check_crabbox_ci_contract.py \ scripts/check_llm_refinement_contract.py \ scripts/build_working_leo_m3taversal_outcome_sandbox.py \ @@ -94,6 +101,7 @@ jobs: tests/test_gcp_infra_execute_canary.py \ tests/test_gcp_infra_readiness_checker.py \ tests/test_gcp_leoclean_runtime_permissions.py \ + tests/test_gcp_leoclean_nosend_package.py \ tests/test_gcp_leoclean_service_environment.py \ tests/test_gcp_runtime_baseline_apply.py \ tests/test_gcp_service_communications.py \ diff --git a/docker/leoclean-nosend/Dockerfile b/docker/leoclean-nosend/Dockerfile new file mode 100644 index 0000000..dd6e511 --- /dev/null +++ b/docker/leoclean-nosend/Dockerfile @@ -0,0 +1,78 @@ +# syntax=docker/dockerfile:1.7 + +FROM ghcr.io/astral-sh/uv:0.9.30@sha256:538e0b39736e7feae937a65983e49d2ab75e1559d35041f9878b7b7e51de91e4 AS uv + +FROM docker.io/library/python:3.11.9-slim-bookworm@sha256:8fb099199b9f2d70342674bd9dbccd3ed03a258f26bbd1d556822c6dfc60c317 AS python-build + +COPY --from=uv /uv /usr/local/bin/uv +COPY artifact/hermes-agent /opt/livingip/leoclean-nosend/artifact/hermes-agent + +RUN /usr/local/bin/uv venv \ + --python /usr/local/bin/python3.11 \ + --no-project \ + /opt/livingip/leoclean-nosend/venv \ + && VIRTUAL_ENV=/opt/livingip/leoclean-nosend/venv /usr/local/bin/uv sync \ + --directory /opt/livingip/leoclean-nosend/artifact/hermes-agent \ + --active \ + --frozen \ + --no-dev \ + --extra cron \ + --no-editable \ + && /usr/local/bin/uv pip check --python /opt/livingip/leoclean-nosend/venv/bin/python + +FROM docker.io/library/postgres:16.14-bookworm@sha256:92620daddcd947f8d5ab5ba66e848702fe443d87fed30c4cea8e389fd78dfc55 + +ARG TELEO_REVISION +ARG ARTIFACT_SHA256 +ARG IDENTITY_SHA256 +ARG INPUT_SHA256 + +LABEL org.opencontainers.image.source="https://github.com/living-ip/teleo-infrastructure" \ + org.opencontainers.image.revision="${TELEO_REVISION}" \ + livingip.artifact.sha256="${ARTIFACT_SHA256}" \ + livingip.identity.sha256="${IDENTITY_SHA256}" \ + livingip.input.sha256="${INPUT_SHA256}" \ + livingip.runtime="leoclean-nosend" \ + livingip.target="gcp-staging" + +COPY --from=python-build /usr/local /usr/local +COPY --from=python-build /opt/livingip/leoclean-nosend/venv /opt/livingip/leoclean-nosend/venv +COPY artifact /opt/livingip/leoclean-nosend/artifact +COPY identity /opt/livingip/leoclean-nosend/identity +COPY entrypoint.py /opt/livingip/leoclean-nosend/entrypoint.py +COPY package_contract.py /opt/livingip/leoclean-nosend/package_contract.py +COPY artifact-receipt.json /opt/livingip/leoclean-nosend/metadata/artifact-receipt.json +COPY image-input.json /opt/livingip/leoclean-nosend/metadata/image-input.json +COPY Dockerfile /opt/livingip/leoclean-nosend/metadata/Dockerfile +COPY cloudsql-server-ca.pem /usr/local/libexec/livingip/leoclean-kb/cloudsql-server-ca.pem + +RUN ln -s /usr/local/bin/python3.11 /usr/bin/python3 \ + && mkdir -p /usr/local/libexec/livingip/leoclean-kb/gcloud-config \ + && chmod 0755 /usr/local/libexec/livingip/leoclean-kb \ + && chmod 0555 /usr/local/libexec/livingip/leoclean-kb/gcloud-config \ + && chmod 0444 /usr/local/libexec/livingip/leoclean-kb/cloudsql-server-ca.pem \ + && chmod 0555 /opt/livingip/leoclean-nosend/entrypoint.py \ + && chmod 0444 /opt/livingip/leoclean-nosend/package_contract.py \ + && case "$TELEO_REVISION" in (*[!0-9a-f]*|'') exit 65;; esac \ + && test "${#TELEO_REVISION}" -eq 40 \ + && for digest in "$ARTIFACT_SHA256" "$IDENTITY_SHA256" "$INPUT_SHA256"; do \ + case "$digest" in (*[!0-9a-f]*|'') exit 65;; esac; \ + test "${#digest}" -eq 64; \ + done \ + && test "$(/usr/local/bin/python3.11 -c 'import platform; print(platform.python_version())')" = "3.11.9" \ + && /usr/bin/psql --version | grep -Eq '^psql \(PostgreSQL\) 16[.]14([[:space:]]|$)' \ + && test "$(sha256sum /usr/local/libexec/livingip/leoclean-kb/cloudsql-server-ca.pem | cut -d ' ' -f 1)" = "80701e768f0e1f6b9d621aa0b53f6e851daaa276c6d9a8e51a300fbc015539cb" \ + && /opt/livingip/leoclean-nosend/venv/bin/python /opt/livingip/leoclean-nosend/entrypoint.py \ + verify-build "$TELEO_REVISION" "$ARTIFACT_SHA256" "$IDENTITY_SHA256" "$INPUT_SHA256" + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONNOUSERSITE=1 \ + PYTHONSAFEPATH=1 + +STOPSIGNAL SIGTERM + +HEALTHCHECK --interval=15s --timeout=10s --start-period=30s --retries=4 \ + CMD ["/opt/livingip/leoclean-nosend/venv/bin/python", "/opt/livingip/leoclean-nosend/entrypoint.py", "health"] + +ENTRYPOINT ["/opt/livingip/leoclean-nosend/venv/bin/python", "/opt/livingip/leoclean-nosend/entrypoint.py"] +CMD ["service"] diff --git a/docker/leoclean-nosend/entrypoint.py b/docker/leoclean-nosend/entrypoint.py new file mode 100644 index 0000000..731b38f --- /dev/null +++ b/docker/leoclean-nosend/entrypoint.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python3 +"""Validate and launch the immutable leoclean no-send container.""" + +from __future__ import annotations + +import ctypes +import importlib.util +import json +import os +import platform +import shutil +import stat +import subprocess +import sys +from pathlib import Path +from typing import Any + + +def _load_adjacent_contract() -> Any: + path = Path(__file__).resolve().with_name("package_contract.py") + spec = importlib.util.spec_from_file_location("livingip_leoclean_nosend_package_contract", path) + if spec is None or spec.loader is None: + raise ImportError("the adjacent package contract cannot be loaded") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +contract = _load_adjacent_contract() + +ROOT = Path(contract.RUNTIME_ROOT) +ARTIFACT = ROOT / "artifact" +IDENTITY = ROOT / "identity" +PROFILE = Path(contract.PROFILE_ROOT) +RECEIPT = ROOT / "metadata" / "artifact-receipt.json" +IMAGE_INPUT = ROOT / "metadata" / "image-input.json" +DOCKERFILE = ROOT / "metadata" / "Dockerfile" +ENTRYPOINT = ROOT / "entrypoint.py" +PACKAGE_CONTRACT = ROOT / "package_contract.py" +CA = Path(contract.CA_DESTINATION) +READINESS = PROFILE / "state" / "readiness.json" +PSQL = Path("/usr/bin/psql") +RUNTIME_PYTHON = ROOT / "venv" / "bin" / "python" +BOOTSTRAP = ARTIFACT / "bootstrap.py" +STATE_DIRECTORIES = ("cache", "cron", "logs", "memories", "platforms", "sessions", "state") + + +class EntrypointError(RuntimeError): + """The installed image or live process violates its release contract.""" + + +def _require(condition: bool, message: str) -> None: + if not condition: + raise EntrypointError(message) + + +def _validate_installed() -> dict[str, Any]: + image_input = contract.load_json(IMAGE_INPUT, "installed image input") + contract.validate_image_input(image_input) + revision = image_input["runtime"]["teleo_git_head"] + runtime = contract.validate_artifact(ARTIFACT, RECEIPT, revision) + for field in ( + "teleo_git_head", + "artifact_sha256", + "artifact_manifest_sha256", + "artifact_receipt_sha256", + "hermes_commit", + "hermes_source_sha256", + "uv_lock_sha256", + ): + _require(runtime.get(field) == image_input["runtime"].get(field), f"installed runtime binding drifted: {field}") + identity = contract.validate_identity_bundle(IDENTITY) + _require(identity == image_input["identity"], "installed identity bundle drifted") + _require(contract.sha256_file(CA) == contract.CA_SHA256, "installed Cloud SQL CA drifted") + _require(platform.python_version() == contract.PYTHON_VERSION, "installed Python version drifted") + _require(Path("/usr/bin/python3").resolve() == Path("/usr/local/bin/python3.11"), "system Python binding drifted") + psql_version = subprocess.run( + [str(PSQL), "--version"], + check=True, + capture_output=True, + text=True, + timeout=10, + ).stdout.strip() + _require( + psql_version.startswith(f"psql (PostgreSQL) {contract.POSTGRES_VERSION}"), "installed psql version drifted" + ) + bindings = image_input["source_bindings"] + _require(contract.sha256_file(DOCKERFILE) == bindings["Dockerfile"], "installed Dockerfile binding drifted") + _require(contract.sha256_file(ENTRYPOINT) == bindings["entrypoint.py"], "installed entrypoint binding drifted") + _require( + contract.sha256_file(PACKAGE_CONTRACT) == bindings["package_contract.py"], + "installed package contract binding drifted", + ) + return image_input + + +def _prepare_profile() -> None: + _require(os.geteuid() == 0, "profile preparation requires the capability-limited container bootstrap user") + _require(PROFILE.is_dir() and not PROFILE.is_symlink(), "runtime profile tmpfs is missing or unsafe") + _require(not any(PROFILE.iterdir()), "runtime profile tmpfs is not empty") + template = ARTIFACT / "profile-template" + for source in template.iterdir(): + destination = PROFILE / source.name + if source.is_dir(): + shutil.copytree(source, destination, symlinks=False) + else: + shutil.copy2(source, destination, follow_symlinks=False) + for name in contract.IDENTITY_FILES: + shutil.copy2(IDENTITY / name, PROFILE / name, follow_symlinks=False) + (PROFILE / name).chmod(0o600) + for name in STATE_DIRECTORIES: + path = PROFILE / name + path.mkdir(mode=0o700) + profile_entries = sorted( + PROFILE.rglob("*"), + key=lambda path: len(path.relative_to(PROFILE).parts), + reverse=True, + ) + for path in profile_entries: + _require(not path.is_symlink(), "prepared runtime profile contains a symlink") + for path in profile_entries: + os.chown(path, contract.RUNTIME_UID, contract.RUNTIME_GID) + runtime_identity = PROFILE / "bin" / "gcp-runtime-identity.json" + os.chown(runtime_identity, 0, 0) + runtime_identity.chmod(0o644) + os.chown(PROFILE, contract.RUNTIME_UID, contract.RUNTIME_GID) + + +def _drop_privileges() -> None: + if os.geteuid() == contract.RUNTIME_UID and os.getegid() == contract.RUNTIME_GID: + return + _require(os.geteuid() == 0, "container bootstrap user is unexpected") + libc = ctypes.CDLL(None, use_errno=True) + pr_capbset_drop = 24 + for capability in (0, 6, 7, 8): # CHOWN, SETGID, SETUID, SETPCAP (last) + if libc.prctl(pr_capbset_drop, capability, 0, 0, 0) != 0: + raise EntrypointError("container bootstrap capability drop failed") + os.setgroups([]) + os.setgid(contract.RUNTIME_GID) + os.setuid(contract.RUNTIME_UID) + _require(os.geteuid() == contract.RUNTIME_UID and os.getegid() == contract.RUNTIME_GID, "privilege drop failed") + + +def _runtime_environment() -> dict[str, str]: + return { + "HOME": str(PROFILE / "state"), + "HERMES_HOME": str(PROFILE), + "LANG": "C", + "LC_ALL": "C", + "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "PYTHONDONTWRITEBYTECODE": "1", + "PYTHONNOUSERSITE": "1", + "PYTHONSAFEPATH": "1", + } + + +def _exec_runtime(command: str) -> None: + _prepare_profile() + _drop_privileges() + argv = [str(RUNTIME_PYTHON), str(BOOTSTRAP), command, "--profile", str(PROFILE)] + if command == "service": + argv.extend(["--readiness-file", str(READINESS)]) + os.execve(str(RUNTIME_PYTHON), argv, _runtime_environment()) + + +def _validate_zero_capabilities() -> None: + status = Path("/proc/1/status").read_text(encoding="utf-8") + fields = {} + for line in status.splitlines(): + key, separator, value = line.partition(":") + if separator: + fields[key] = value.strip() + for name in ("CapInh", "CapPrm", "CapEff", "CapBnd", "CapAmb"): + _require(fields.get(name) == "0000000000000000", f"runtime process retains {name}") + _require(fields.get("NoNewPrivs") == "1", "runtime process does not enforce no-new-privileges") + _require( + fields.get("Uid") + == f"{contract.RUNTIME_UID}\t{contract.RUNTIME_UID}\t{contract.RUNTIME_UID}\t{contract.RUNTIME_UID}", + "runtime uid drifted", + ) + _require( + fields.get("Gid") + == f"{contract.RUNTIME_GID}\t{contract.RUNTIME_GID}\t{contract.RUNTIME_GID}\t{contract.RUNTIME_GID}", + "runtime gid drifted", + ) + + +def _health() -> dict[str, Any]: + _drop_privileges() + _validate_zero_capabilities() + _require(READINESS.is_file() and not READINESS.is_symlink(), "runtime readiness receipt is missing or unsafe") + _require(stat.S_IMODE(READINESS.stat().st_mode) == 0o600, "runtime readiness receipt mode drifted") + value = contract.load_json(READINESS, "runtime readiness receipt") + _require( + set(value) + == { + "schema", + "status", + "process_id", + "runtime_mode", + "config_sha256", + "gateway_adapter_count", + "connected_platforms", + "tool_surface", + "identity_sha256", + }, + "runtime readiness fields are not exact", + ) + _require(value.get("schema") == "livingip.leocleanNoSendServiceReadiness.v1", "runtime readiness schema drifted") + _require( + value.get("status") == "ready" and value.get("process_id") == 1, "runtime readiness process binding drifted" + ) + _require(value.get("runtime_mode") == "gcp_staging_no_send", "runtime readiness mode drifted") + _require( + value.get("gateway_adapter_count") == 0 and value.get("connected_platforms") == [], + "runtime has a transport adapter", + ) + surface = value.get("tool_surface") + _require(isinstance(surface, dict), "runtime tool surface is missing") + _require(surface.get("tools") == ["skill_view", "skills_list", "terminal"], "runtime tool surface drifted") + _require(surface.get("send_message_present") is False, "runtime send_message surface is present") + expected_identity = {name: contract.sha256_file(PROFILE / name) for name in contract.IDENTITY_FILES} + _require(value.get("identity_sha256") == expected_identity, "runtime readiness identity binding drifted") + cmdline = Path("/proc/1/cmdline").read_bytes().split(b"\0") + expected_native = [ + str(RUNTIME_PYTHON).encode(), + str(BOOTSTRAP).encode(), + b"service", + b"--profile", + str(PROFILE).encode(), + b"--readiness-file", + str(READINESS).encode(), + b"", + ] + expected_binfmt = [expected_native[0], *expected_native] + _require(cmdline in (expected_native, expected_binfmt), "runtime process command line drifted") + return { + "schema": "livingip.leocleanNoSendContainerHealth.v1", + "status": "pass", + "process_id": 1, + "gateway_adapter_count": 0, + "send_message_present": False, + } + + +def _verification_result(image_input: dict[str, Any]) -> dict[str, Any]: + return { + "schema": "livingip.leocleanNoSendContainerVerification.v1", + "status": "pass", + "teleo_git_head": image_input["runtime"]["teleo_git_head"], + "artifact_sha256": image_input["runtime"]["artifact_sha256"], + "identity_sha256": image_input["identity"]["bundle_sha256"], + "input_sha256": image_input["input_sha256"], + "python_version": contract.PYTHON_VERSION, + "postgres_version": contract.POSTGRES_VERSION, + "cloudsql_ca_sha256": contract.CA_SHA256, + } + + +def main(argv: list[str] | None = None) -> int: + arguments = argv if argv is not None else sys.argv[1:] + arguments = arguments or ["service"] + command = arguments[0] + try: + image_input = _validate_installed() + if command in {"probe", "service"}: + _require(len(arguments) == 1, "runtime command arguments are not exact") + _exec_runtime(command) + if command == "health": + _require(len(arguments) == 1, "health command arguments are not exact") + result = _health() + elif command == "verify": + _require(len(arguments) == 1, "verify command arguments are not exact") + result = _verification_result(image_input) + elif command == "verify-build": + _require(len(arguments) == 5, "build verification arguments are not exact") + expected = ( + image_input["runtime"]["teleo_git_head"], + image_input["runtime"]["artifact_sha256"], + image_input["identity"]["bundle_sha256"], + image_input["input_sha256"], + ) + _require(tuple(arguments[1:]) == expected, "image build arguments drifted from image input") + result = _verification_result(image_input) + else: + raise EntrypointError("unsupported container command") + print(json.dumps(result, separators=(",", ":"), sort_keys=True)) + return 0 + except (EntrypointError, contract.PackageError, OSError, subprocess.SubprocessError): + print(json.dumps({"status": "fail", "error": "container_contract_failed"}, sort_keys=True), file=sys.stderr) + return 65 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/docs/gcp-leoclean-nosend-runtime.md b/docs/gcp-leoclean-nosend-runtime.md index 724762d..637142c 100644 --- a/docs/gcp-leoclean-nosend-runtime.md +++ b/docs/gcp-leoclean-nosend-runtime.md @@ -94,3 +94,7 @@ Those remain separate rollback boundaries: projection, and runtime receipts; and 6. cut over only after soak and restore testing, then revoke the VPS credentials and retire the VPS. + +The independently reversible OCI/systemd packaging contract for step 2 is +documented in `docs/gcp-leoclean-nosend-service-package.md`. Its offline proof +does not count as a live deployment or satisfy steps 1, 3, 4, 5, or 6. diff --git a/docs/gcp-leoclean-nosend-service-package.md b/docs/gcp-leoclean-nosend-service-package.md new file mode 100644 index 0000000..0936fa8 --- /dev/null +++ b/docs/gcp-leoclean-nosend-service-package.md @@ -0,0 +1,138 @@ +# GCP leoclean no-send service package + +## Outcome + +This slice packages the revision-bound Hermes/leoclean no-send artifact and one +explicit identity bundle as an immutable OCI image for a separate +`leoclean-gcp-nosend.service` on `teleo-staging-1`. + +It is target-architecture staging work, not a production cutover. The image is +the atomic compatibility boundary because Python, the locked Hermes virtual +environment, PostgreSQL client, Cloud SQL CA, runtime artifact, identity files, +and container entrypoint must move and roll back together. + +## Exact target and authority boundary + +- project: `teleo-501523` +- zone: `europe-west6-a` +- instance: `teleo-staging-1` +- service account: + `sa-teleo-staging-vm@teleo-501523.iam.gserviceaccount.com` +- service: `leoclean-gcp-nosend.service` +- platform: `linux/amd64` +- image repository: + `europe-west6-docker.pkg.dev/teleo-501523/teleo/leoclean-nosend-staging` + +The package adds only a deployable, digest-addressed staging service surface. +It does not add IAM, Secret Manager, database-role, proposal-approval, +canonical-apply, transport, production, or VPS authority. + +The service has no provider credential in this slice. It can prove the gateway +and database-tool boundary, but paid model calls require a separately reviewed +internal no-send ingress and credential-delivery design during the parity +slice. + +## Immutable closure + +The dedicated Dockerfile uses only digest-pinned base images: + +- Python `3.11.9`; +- uv `0.9.30`; +- PostgreSQL client `16.14` on Bookworm. + +There is no `apt-get`, floating `pip install`, editable install, or image tag in +the closure. The final image also binds: + +- the exact Teleo Git revision and release-pass artifact receipt; +- Hermes commit and source-tree hash; +- the artifact content and manifest hashes; +- the exact uv lock; +- the identity manifest, lock, `SOUL.md`, and bundle hashes; +- the Cloud SQL server CA hash; +- the Dockerfile, entrypoint, and package-contract hashes. + +The runtime image starts with a capability-limited bootstrap that creates a +fresh tmpfs profile, then drops all groups, UID/GID to `65532`, and all Linux +capability bounding sets before executing Hermes. The gateway readiness receipt +is written only after the real zero-adapter runner starts and its sealed tool +registry is rechecked. + +The PostgreSQL base image's inherited data-volume path is explicitly shadowed +by a tiny read-only, non-executable, mode-`0000` tmpfs. This prevents Docker +from exposing an unused writable database directory to the runtime. + +## Prepare a build context + +First compile and release-verify the no-send artifact from the exact clean +revision. Separately provide a reviewed identity directory containing exactly: + +- `SOUL.md` +- `identity-lock.json` +- `identity-manifest.json` + +Then run: + +```bash +python3 ops/gcp_leoclean_nosend_package.py prepare \ + --artifact /path/to/artifact \ + --receipt /path/to/leoclean-nosend-runtime.json \ + --identity /path/to/identity-bundle \ + --output /tmp/leoclean-nosend-image-context +``` + +The command fails closed on a development receipt, revision drift, content or +mode drift, unsafe links, an inconsistent identity lock, a changed CA, a +non-staging target, or an existing output directory. The resulting context is +secret-free. + +Use the exact values in `build-args.json` as Docker build arguments. The image +build itself revalidates that those values equal `image-input.json`. + +## Finalize one digest-bound release + +After an authorized build and Artifact Registry push returns the immutable +image digest: + +```bash +python3 ops/gcp_leoclean_nosend_package.py finalize \ + --image-input /tmp/leoclean-nosend-image-context/image-input.json \ + --image-reference \ + europe-west6-docker.pkg.dev/teleo-501523/teleo/leoclean-nosend-staging@sha256:<64-hex-digest> \ + --output-release /tmp/leoclean-nosend-release.json \ + --output-unit /tmp/leoclean-gcp-nosend.service +``` + +Tags, alternate repositories, alternate projects/instances/service accounts, +extra descriptor fields, and mismatched labels are rejected. + +## Offline evidence and live gate + +Offline tests prove descriptor strictness, artifact and identity binding, +digest-only images, pinned runtime versions, a secret-free context, the +capability-drop contract, no published ports, no Docker-socket mount, no +environment file, and no production/VPS/Telegram target. + +They do not prove that `teleo-staging-1` currently has a compatible container +engine, enough disk, the expected architecture, the expected service account, +or an unused unit name. Those are the next read-only live preflight. If the +engine is absent, installation is separate infrastructure work; the deploy must +stop instead of falling back to the production sync script or a mutable host +virtual environment. + +Publishing the draft branch and stacked PR is only a review handoff. Live +installation, Artifact Registry publication, service restart, IAM/secret +changes, Cloud SQL access, and proposal staging remain separate approvals. + +## Rollback and sunset + +The rollback boundary is one release descriptor, one image digest, and the +dedicated no-send unit. Rollback restores the prior descriptor/unit and prior +digest; it must never restart or edit the production Leo, Telegram, or VPS +services. + +The fresh tmpfs profile intentionally has no session continuity. That is +appropriate for isolated gateway/database and fresh-turn parity proof, but it +is not the production state design. Before traffic cutover, a separate reviewed +slice must bind durable noncanonical session state without making it identity or +knowledge authority. The tmpfs-only state contract sunsets when that cutover +candidate is proven. diff --git a/docs/workstreams/gcp-leoclean/goals.md b/docs/workstreams/gcp-leoclean/goals.md new file mode 100644 index 0000000..a4e82ef --- /dev/null +++ b/docs/workstreams/gcp-leoclean/goals.md @@ -0,0 +1,50 @@ +# GCP leoclean migration goal + +## Objective + +Deploy and prove Leo's least-privilege Hermes/leoclean runtime in GCP staging +against canonical Cloud SQL, establish behavioral and database parity, connect +the protected Observatory to canonical/proposal/vector/runtime receipts, then +cut over safely and retire the VPS after soak. + +## Architecture baseline + +- Hermes with the `leoclean` profile remains Leo's intended runtime. +- Cloud SQL is canonical PostgreSQL authority. +- The protected Observatory is a separate frontend surface. +- The VPS remains untouched until an explicitly approved, parity-proven + cutover and retirement sequence. + +## Current state + +- Complete: PR #179 least-privilege PostgreSQL authority is merged on main. +- Complete: the revision-bound, structurally no-send runtime from PR #183 is + merged into `main` at `2d88c9765bd9b43641d1efb04a1118c5ce0bce39`. +- Rebuilt for human review: isolated, digest-addressed GCP staging service + package with an independent rollback boundary, exact release receipt, + immutable image verification, and network-disabled service/readiness smoke + test, based directly on that merged `main` revision in PR #182. +- In progress: validate the rebuilt PR #182 revision and stop at the + exact-revision human review and merge gate. +- Not yet live-proven: staging VM/container engine, IAM/Secret Manager, Cloud + SQL effective identity, restart behavior, or model/database parity. + +## Remaining order + +1. Obtain exact-revision human review for the no-send service-package stack. +2. Run the read-only `teleo-staging-1` host/engine/service-account preflight. +3. Provision the scoped Cloud SQL role/secret and deploy the separate staging + service. +4. Prove effective identity, reads, function-only proposal staging, denied + writes/escalation, receipts, restart stability, and rollback. +5. Run behavioral and database parity backtesting. +6. Connect the protected Observatory to canonical claims, proposals, pgvector + projection, and runtime receipts. +7. Cut over, soak, restore-test, revoke VPS credentials, and retire the VPS. + +## Current live-action boundary + +Draft branch/PR publication is authorized only as a review handoff. No merge, +Artifact Registry push, GCP/Cloud SQL/Secret Manager mutation, service +deployment/restart, Telegram change, production promotion, canonical proposal +apply, or VPS access is part of the current offline slice. diff --git a/hermes-agent/runtime/leoclean-nosend/bootstrap.py b/hermes-agent/runtime/leoclean-nosend/bootstrap.py index a6cad4f..2bea04b 100644 --- a/hermes-agent/runtime/leoclean-nosend/bootstrap.py +++ b/hermes-agent/runtime/leoclean-nosend/bootstrap.py @@ -15,9 +15,12 @@ import signal import stat import subprocess import sys +import tempfile from pathlib import Path from typing import Any +READINESS_SCHEMA = "livingip.leocleanNoSendServiceReadiness.v1" + class NoSendContractError(RuntimeError): """The runtime does not satisfy its no-send contract.""" @@ -738,7 +741,58 @@ def probe(profile: Path, *, template: bool) -> dict[str, Any]: } -async def _run_service(profile: Path) -> bool: +def _write_readiness( + profile: Path, + readiness_file: Path, + contract: dict[str, Any], + runner: Any, + tool_surface: dict[str, Any], +) -> None: + state = profile / "state" + _require(state.is_dir() and not state.is_symlink(), "service state directory is missing or unsafe") + expected_parent = state.resolve(strict=True) + _require( + readiness_file.parent.resolve(strict=True) == expected_parent, "readiness file must be inside profile state" + ) + _require(readiness_file.name == "readiness.json", "readiness file name is not exact") + _require(not readiness_file.is_symlink(), "readiness file cannot be a symlink") + identity_hashes = {} + for name in ("SOUL.md", "identity-lock.json", "identity-manifest.json"): + path = profile / name + _require(path.is_file() and not path.is_symlink(), f"runtime identity entry is unsafe: {name}") + identity_hashes[name] = _sha256(path) + payload = { + "schema": READINESS_SCHEMA, + "status": "ready", + "process_id": os.getpid(), + "runtime_mode": contract["runtime_mode"], + "config_sha256": _sha256(profile / "config.yaml"), + "gateway_adapter_count": len(runner.adapters), + "connected_platforms": [], + "tool_surface": tool_surface, + "identity_sha256": identity_hashes, + } + encoded = (json.dumps(payload, separators=(",", ":"), sort_keys=True) + "\n").encode("utf-8") + descriptor = None + temporary = None + try: + descriptor, temporary = tempfile.mkstemp(prefix=".readiness.", dir=expected_parent) + os.fchmod(descriptor, 0o600) + with os.fdopen(descriptor, "wb", closefd=True) as handle: + descriptor = None + handle.write(encoded) + handle.flush() + os.fsync(handle.fileno()) + os.replace(temporary, readiness_file) + temporary = None + finally: + if descriptor is not None: + os.close(descriptor) + if temporary is not None: + Path(temporary).unlink(missing_ok=True) + + +async def _run_service(profile: Path, readiness_file: Path | None = None) -> bool: contract, _config, _surface, _effective_policy = _prepare(profile, template=False) from gateway.config import GatewayConfig from gateway.run import GatewayRunner @@ -749,7 +803,7 @@ async def _run_service(profile: Path) -> bool: _require(getattr(runner, "_smart_model_routing", None) == {}, "service retained smart-model routing") _assert_tool_registry(profile, contract) importlib.import_module("model_tools") - _assert_tool_registry(profile, contract) + tool_surface = _assert_tool_registry(profile, contract) loop = asyncio.get_running_loop() stop_tasks: set[asyncio.Task[Any]] = set() @@ -766,9 +820,15 @@ async def _run_service(profile: Path) -> bool: pass started = await runner.start() _require(started, "Hermes no-send gateway did not start") - _assert_tool_registry(profile, contract) - await runner.wait_for_shutdown() - return True + tool_surface = _assert_tool_registry(profile, contract) + if readiness_file is not None: + _write_readiness(profile, readiness_file, contract, runner, tool_surface) + try: + await runner.wait_for_shutdown() + return True + finally: + if readiness_file is not None: + readiness_file.unlink(missing_ok=True) def main() -> int: @@ -779,12 +839,15 @@ def main() -> int: child.add_argument("--profile", required=True, type=Path) if name == "probe": child.add_argument("--template", action="store_true") + else: + child.add_argument("--readiness-file", type=Path) args = parser.parse_args() try: if args.command == "probe": print(json.dumps(probe(args.profile.resolve(), template=args.template), sort_keys=True)) return 0 - return 0 if asyncio.run(_run_service(args.profile.resolve())) else 1 + readiness_file = args.readiness_file.absolute() if args.readiness_file else None + return 0 if asyncio.run(_run_service(args.profile.resolve(), readiness_file)) else 1 except NoSendContractError as exc: print(json.dumps({"status": "fail", "error": str(exc)}, sort_keys=True), file=sys.stderr) return 65 diff --git a/hermes-agent/runtime/leoclean-nosend/runtime-lock.json b/hermes-agent/runtime/leoclean-nosend/runtime-lock.json index c2115df..9ba1097 100644 --- a/hermes-agent/runtime/leoclean-nosend/runtime-lock.json +++ b/hermes-agent/runtime/leoclean-nosend/runtime-lock.json @@ -34,7 +34,7 @@ { "source": "hermes-agent/runtime/leoclean-nosend/profile-template/config.yaml", "target": "config.yaml", - "mode": "0600" + "mode": "0444" }, { "source": "hermes-agent/leoclean-bin/teleo-kb-gcp", diff --git a/ops/gcp_leoclean_nosend_package.py b/ops/gcp_leoclean_nosend_package.py new file mode 100644 index 0000000..0fb84f5 --- /dev/null +++ b/ops/gcp_leoclean_nosend_package.py @@ -0,0 +1,674 @@ +#!/usr/bin/env python3 +"""Build and validate the immutable GCP leoclean no-send service package.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +import shutil +import stat +import subprocess +import sys +from collections.abc import Mapping +from pathlib import Path +from typing import Any + +IMAGE_INPUT_SCHEMA = "livingip.leocleanNoSendImageInput.v1" +RELEASE_SCHEMA = "livingip.leocleanNoSendRelease.v1" +ARTIFACT_MANIFEST_SCHEMA = "livingip.leocleanNoSendArtifactManifest.v1" +ARTIFACT_RECEIPT_SCHEMA = "livingip.leocleanNoSendArtifactVerification.v1" +IDENTITY_MANIFEST_SCHEMA = "livingip.leoIdentityManifest.v1" +IDENTITY_LOCK_SCHEMA = "livingip.leoIdentityProfileLock.v1" + +PROJECT = "teleo-501523" +ZONE = "europe-west6-a" +INSTANCE = "teleo-staging-1" +SERVICE = "leoclean-gcp-nosend.service" +SERVICE_ACCOUNT = "sa-teleo-staging-vm@teleo-501523.iam.gserviceaccount.com" +PLATFORM = "linux/amd64" +IMAGE_REPOSITORY = "europe-west6-docker.pkg.dev/teleo-501523/teleo/leoclean-nosend-staging" +CONTAINER_NAME = "livingip-leoclean-nosend" + +HERMES_COMMIT = "b2f477a30b3c05d0f383c543af98496ae8a96070" +PYTHON_VERSION = "3.11.9" +UV_VERSION = "0.9.30" +POSTGRES_VERSION = "16.14" +PYTHON_IMAGE = ( + "docker.io/library/python:3.11.9-slim-bookworm@" + "sha256:8fb099199b9f2d70342674bd9dbccd3ed03a258f26bbd1d556822c6dfc60c317" +) +UV_IMAGE = "ghcr.io/astral-sh/uv:0.9.30@sha256:538e0b39736e7feae937a65983e49d2ab75e1559d35041f9878b7b7e51de91e4" +POSTGRES_IMAGE = ( + "docker.io/library/postgres:16.14-bookworm@sha256:92620daddcd947f8d5ab5ba66e848702fe443d87fed30c4cea8e389fd78dfc55" +) +CA_SHA256 = "80701e768f0e1f6b9d621aa0b53f6e851daaa276c6d9a8e51a300fbc015539cb" +CA_DESTINATION = "/usr/local/libexec/livingip/leoclean-kb/cloudsql-server-ca.pem" +RUNTIME_ROOT = "/opt/livingip/leoclean-nosend" +PROFILE_ROOT = "/run/leoclean-profile" +RUNTIME_UID = 65532 +RUNTIME_GID = 65532 + +HEX_40 = re.compile(r"^[0-9a-f]{40}$") +HEX_64 = re.compile(r"^[0-9a-f]{64}$") +IMAGE_REFERENCE = re.compile(rf"^{re.escape(IMAGE_REPOSITORY)}@sha256:([0-9a-f]{{64}})$") +FORBIDDEN_DESCRIPTOR_MARKERS = ( + "77.42.65.182", + "gcp-teleo-pgvector-standby-postgres-password", + "leoclean-gateway.service", + "sa-teleo-prod-vm", + "telegram_bot_token", + "teleo-prod-1", +) +IDENTITY_FILES = ("SOUL.md", "identity-lock.json", "identity-manifest.json") + + +class PackageError(RuntimeError): + """The image or service package does not satisfy the staging contract.""" + + +def _require(condition: bool, message: str) -> None: + if not condition: + raise PackageError(message) + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def canonical_sha256(value: Any) -> str: + encoded = json.dumps(value, ensure_ascii=True, separators=(",", ":"), sort_keys=True).encode("utf-8") + return hashlib.sha256(encoded).hexdigest() + + +def load_json(path: Path, label: str) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise PackageError(f"cannot read {label}: {type(exc).__name__}") from exc + _require(isinstance(value, dict), f"{label} must be a JSON object") + return value + + +def write_json(path: Path, value: Mapping[str, Any], mode: int = 0o644) -> None: + path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8") + path.chmod(mode) + + +def _is_sha256(value: Any) -> bool: + return isinstance(value, str) and bool(HEX_64.fullmatch(value)) + + +def _is_revision(value: Any) -> bool: + return isinstance(value, str) and bool(HEX_40.fullmatch(value)) + + +def _safe_tree(root: Path, label: str) -> None: + _require(root.is_dir() and not root.is_symlink(), f"{label} root is missing or unsafe") + for path in root.rglob("*"): + _require(not path.is_symlink(), f"{label} contains a symlink") + _require(path.is_dir() or path.is_file(), f"{label} contains an unsupported entry") + + +def tree_manifest(root: Path, *, excluded: frozenset[str] = frozenset()) -> dict[str, Any]: + _safe_tree(root, "tree") + files: list[dict[str, Any]] = [] + for path in sorted(root.rglob("*"), key=lambda item: item.relative_to(root).as_posix()): + relative = path.relative_to(root).as_posix() + if relative in excluded or path.is_dir(): + continue + mode = stat.S_IMODE(path.stat().st_mode) + files.append( + { + "path": relative, + "bytes": path.stat().st_size, + "mode": f"0o{mode:03o}", + "sha256": sha256_file(path), + } + ) + stable = {"files": files, "file_count": len(files), "total_bytes": sum(item["bytes"] for item in files)} + return {**stable, "sha256": canonical_sha256(stable)} + + +def identity_bundle_manifest(root: Path) -> dict[str, Any]: + _safe_tree(root, "identity bundle") + actual = sorted(path.name for path in root.iterdir()) + _require(actual == sorted(IDENTITY_FILES), "identity bundle entry set is not exact") + files = [] + for name in IDENTITY_FILES: + path = root / name + _require(path.is_file() and not path.is_symlink(), f"identity file is unsafe: {name}") + _require(path.stat().st_size <= 1024 * 1024, f"identity file is too large: {name}") + _require(not stat.S_IMODE(path.stat().st_mode) & 0o022, f"identity file is group/world writable: {name}") + files.append({"path": name, "bytes": path.stat().st_size, "sha256": sha256_file(path)}) + stable = {"files": files, "file_count": len(files), "total_bytes": sum(item["bytes"] for item in files)} + return {**stable, "sha256": canonical_sha256(stable)} + + +def validate_identity_bundle(root: Path) -> dict[str, Any]: + bundle = identity_bundle_manifest(root) + manifest = load_json(root / "identity-manifest.json", "identity manifest") + lock = load_json(root / "identity-lock.json", "identity lock") + _require( + set(manifest) == {"schema", "identity_inputs", "identity_inputs_sha256", "compiled_views", "manifest_sha256"}, + "identity manifest fields are not exact", + ) + _require(manifest.get("schema") == IDENTITY_MANIFEST_SCHEMA, "identity manifest schema is invalid") + _require(_is_sha256(manifest.get("manifest_sha256")), "identity manifest hash is invalid") + _require(_is_sha256(manifest.get("identity_inputs_sha256")), "identity input hash is invalid") + stable = {key: value for key, value in manifest.items() if key != "manifest_sha256"} + _require(canonical_sha256(stable) == manifest["manifest_sha256"], "identity manifest self-hash drifted") + views = manifest.get("compiled_views") + _require( + isinstance(views, dict) and set(views) == {"SOUL.md", "identity.json"}, "compiled identity views are incomplete" + ) + soul = views["SOUL.md"] if isinstance(views, dict) else None + _require(isinstance(soul, dict) and _is_sha256(soul.get("sha256")), "SOUL identity binding is invalid") + _require(soul["sha256"] == sha256_file(root / "SOUL.md"), "SOUL content differs from identity manifest") + _require( + set(lock) == {"schema", "manifest_sha256", "identity_inputs_sha256", "compiled_views", "session_boundary"}, + "identity lock fields are not exact", + ) + _require(lock.get("schema") == IDENTITY_LOCK_SCHEMA, "identity lock schema is invalid") + _require(lock.get("manifest_sha256") == manifest["manifest_sha256"], "identity lock manifest binding drifted") + _require( + lock.get("identity_inputs_sha256") == manifest["identity_inputs_sha256"], + "identity lock input binding drifted", + ) + _require( + isinstance(lock.get("compiled_views"), dict) + and (lock["compiled_views"].get("SOUL.md") or {}).get("sha256") == soul["sha256"], + "identity lock SOUL binding drifted", + ) + inputs = manifest.get("identity_inputs") + database = inputs.get("canonical_database") if isinstance(inputs, dict) else None + _require(isinstance(database, dict), "identity database authority is missing") + authority = database.get("authority") + canonical = database.get("canonical_authority_granted") + _require( + (authority, canonical) + in { + ("synthetic_noncanonical_fixture", False), + ("canonical_database_same_transaction_export", True), + }, + "identity database authority is invalid", + ) + return { + "bundle_sha256": bundle["sha256"], + "manifest_sha256": manifest["manifest_sha256"], + "identity_inputs_sha256": manifest["identity_inputs_sha256"], + "soul_sha256": soul["sha256"], + "authority": authority, + "canonical_authority_granted": canonical, + } + + +def validate_artifact(artifact: Path, receipt_path: Path, expected_revision: str) -> dict[str, Any]: + _require(_is_revision(expected_revision), "expected Teleo revision is invalid") + _safe_tree(artifact, "runtime artifact") + manifest = load_json(artifact / "artifact-manifest.json", "artifact manifest") + receipt = load_json(receipt_path, "artifact verification receipt") + _require(manifest.get("schema") == ARTIFACT_MANIFEST_SCHEMA, "artifact manifest schema is invalid") + _require(receipt.get("schema") == ARTIFACT_RECEIPT_SCHEMA, "artifact receipt schema is invalid") + _require(receipt.get("status") == "pass", "artifact receipt did not pass") + _require(receipt.get("verification_mode") == "release", "artifact receipt is not release evidence") + _require(manifest.get("teleo_git_head") == expected_revision, "artifact revision differs from source revision") + _require(receipt.get("teleo_git_head") == expected_revision, "receipt revision differs from source revision") + content = tree_manifest(artifact, excluded=frozenset({"artifact-manifest.json"})) + _require(manifest.get("content") == content, "artifact content manifest drifted") + _require(receipt.get("artifact_sha256") == content["sha256"], "artifact receipt content hash drifted") + _require(manifest.get("hermes_commit") == HERMES_COMMIT, "artifact Hermes commit drifted") + _require(receipt.get("hermes_commit") == HERMES_COMMIT, "receipt Hermes commit drifted") + runtime_lock = load_json(artifact / "runtime-lock.json", "artifact runtime lock") + _require( + runtime_lock.get("base_images") == {"python": PYTHON_IMAGE.removeprefix("docker.io/library/"), "uv": UV_IMAGE}, + "artifact base-image lock drifted", + ) + hermes = runtime_lock.get("hermes") + _require(isinstance(hermes, dict) and hermes.get("commit") == HERMES_COMMIT, "artifact Hermes lock drifted") + claim_ceiling = manifest.get("claim_ceiling") + _require(isinstance(claim_ceiling, str) and claim_ceiling == receipt.get("claim_ceiling"), "claim ceiling drifted") + return { + "teleo_git_head": expected_revision, + "artifact_sha256": content["sha256"], + "artifact_manifest_sha256": sha256_file(artifact / "artifact-manifest.json"), + "artifact_receipt_sha256": sha256_file(receipt_path), + "hermes_commit": HERMES_COMMIT, + "hermes_source_sha256": receipt.get("hermes_source_sha256"), + "uv_lock_sha256": hermes.get("uv_lock_sha256"), + "claim_ceiling": claim_ceiling, + } + + +def _target() -> dict[str, str]: + return { + "project": PROJECT, + "zone": ZONE, + "instance": INSTANCE, + "service": SERVICE, + "service_account": SERVICE_ACCOUNT, + "platform": PLATFORM, + } + + +def _base_images() -> dict[str, str]: + return {"python": PYTHON_IMAGE, "uv": UV_IMAGE, "postgres": POSTGRES_IMAGE} + + +def _assert_no_forbidden_descriptor_markers(value: Mapping[str, Any]) -> None: + encoded = json.dumps(value, ensure_ascii=True, separators=(",", ":"), sort_keys=True).casefold() + for marker in FORBIDDEN_DESCRIPTOR_MARKERS: + _require( + marker.casefold() not in encoded, "release descriptor contains a forbidden production/transport marker" + ) + + +def validate_image_input(value: dict[str, Any]) -> None: + _require( + set(value) + == { + "schema", + "target", + "runtime", + "identity", + "base_images", + "source_bindings", + "claim_ceiling", + "input_sha256", + }, + "image input fields are not exact", + ) + _require(value.get("schema") == IMAGE_INPUT_SCHEMA, "image input schema is invalid") + _require(value.get("target") == _target(), "image input target is not exact GCP staging") + _require(value.get("base_images") == _base_images(), "image input base-image closure drifted") + runtime = value.get("runtime") + _require(isinstance(runtime, dict), "image runtime binding is missing") + _require( + set(runtime) + == { + "teleo_git_head", + "artifact_sha256", + "artifact_manifest_sha256", + "artifact_receipt_sha256", + "hermes_commit", + "hermes_source_sha256", + "uv_lock_sha256", + "python_version", + "uv_version", + "postgres_version", + "cloudsql_ca_sha256", + }, + "image runtime binding fields are not exact", + ) + _require(_is_revision(runtime.get("teleo_git_head")), "image Teleo revision is invalid") + for field in ( + "artifact_sha256", + "artifact_manifest_sha256", + "artifact_receipt_sha256", + "hermes_source_sha256", + "uv_lock_sha256", + "cloudsql_ca_sha256", + ): + _require(_is_sha256(runtime.get(field)), f"image runtime {field} is invalid") + _require(runtime.get("hermes_commit") == HERMES_COMMIT, "image Hermes commit drifted") + _require(runtime.get("python_version") == PYTHON_VERSION, "image Python version drifted") + _require(runtime.get("uv_version") == UV_VERSION, "image uv version drifted") + _require(runtime.get("postgres_version") == POSTGRES_VERSION, "image PostgreSQL version drifted") + _require(runtime.get("cloudsql_ca_sha256") == CA_SHA256, "image Cloud SQL CA drifted") + identity = value.get("identity") + _require( + isinstance(identity, dict) + and set(identity) + == { + "bundle_sha256", + "manifest_sha256", + "identity_inputs_sha256", + "soul_sha256", + "authority", + "canonical_authority_granted", + }, + "image identity binding fields are not exact", + ) + for field in ("bundle_sha256", "manifest_sha256", "identity_inputs_sha256", "soul_sha256"): + _require(_is_sha256(identity.get(field)), f"image identity {field} is invalid") + _require( + (identity.get("authority"), identity.get("canonical_authority_granted")) + in { + ("synthetic_noncanonical_fixture", False), + ("canonical_database_same_transaction_export", True), + }, + "image identity authority is invalid", + ) + bindings = value.get("source_bindings") + _require( + isinstance(bindings, dict) + and set(bindings) == {"Dockerfile", "entrypoint.py", "package_contract.py"} + and all(_is_sha256(item) for item in bindings.values()), + "image packaging source bindings are invalid", + ) + _require( + isinstance(value.get("claim_ceiling"), str) and bool(value["claim_ceiling"]), "image claim ceiling is missing" + ) + stable = {key: item for key, item in value.items() if key != "input_sha256"} + _require(value.get("input_sha256") == canonical_sha256(stable), "image input self-hash drifted") + _assert_no_forbidden_descriptor_markers(value) + + +def build_image_input( + *, + artifact: Path, + receipt: Path, + identity: Path, + ca: Path, + source_revision: str, + dockerfile: Path, + entrypoint: Path, + package_contract: Path, +) -> dict[str, Any]: + _require(sha256_file(ca) == CA_SHA256, "Cloud SQL CA does not match the reviewed certificate") + runtime = validate_artifact(artifact, receipt, source_revision) + runtime.update( + { + "python_version": PYTHON_VERSION, + "uv_version": UV_VERSION, + "postgres_version": POSTGRES_VERSION, + "cloudsql_ca_sha256": CA_SHA256, + } + ) + identity_binding = validate_identity_bundle(identity) + stable = { + "schema": IMAGE_INPUT_SCHEMA, + "target": _target(), + "runtime": {key: runtime[key] for key in runtime if key != "claim_ceiling"}, + "identity": identity_binding, + "base_images": _base_images(), + "source_bindings": { + "Dockerfile": sha256_file(dockerfile), + "entrypoint.py": sha256_file(entrypoint), + "package_contract.py": sha256_file(package_contract), + }, + "claim_ceiling": runtime["claim_ceiling"], + } + value = {**stable, "input_sha256": canonical_sha256(stable)} + validate_image_input(value) + return value + + +def prepare_context( + *, + repo_root: Path, + artifact: Path, + receipt: Path, + identity: Path, + output: Path, + source_revision: str, +) -> dict[str, Any]: + _require(not output.exists() and not output.is_symlink(), "output context already exists") + dockerfile = repo_root / "docker" / "leoclean-nosend" / "Dockerfile" + entrypoint = repo_root / "docker" / "leoclean-nosend" / "entrypoint.py" + package_contract = repo_root / "ops" / "gcp_leoclean_nosend_package.py" + ca = repo_root / "ops" / "gcp-teleo-pgvector-standby-server-ca.pem" + for path, label in ( + (dockerfile, "Dockerfile"), + (entrypoint, "entrypoint"), + (package_contract, "package contract"), + (ca, "Cloud SQL CA"), + ): + _require(path.is_file() and not path.is_symlink(), f"{label} source is missing or unsafe") + image_input = build_image_input( + artifact=artifact, + receipt=receipt, + identity=identity, + ca=ca, + source_revision=source_revision, + dockerfile=dockerfile, + entrypoint=entrypoint, + package_contract=package_contract, + ) + output.mkdir(parents=True, mode=0o700) + try: + shutil.copytree(artifact, output / "artifact", symlinks=False) + shutil.copytree(identity, output / "identity", symlinks=False) + for name in IDENTITY_FILES: + (output / "identity" / name).chmod(0o600) + shutil.copyfile(receipt, output / "artifact-receipt.json") + shutil.copyfile(ca, output / "cloudsql-server-ca.pem") + shutil.copyfile(dockerfile, output / "Dockerfile") + shutil.copyfile(entrypoint, output / "entrypoint.py") + shutil.copyfile(package_contract, output / "package_contract.py") + for name in ( + "artifact-receipt.json", + "cloudsql-server-ca.pem", + "Dockerfile", + "entrypoint.py", + "package_contract.py", + ): + (output / name).chmod(0o644) + write_json(output / "image-input.json", image_input) + write_json( + output / "build-args.json", + { + "ARTIFACT_SHA256": image_input["runtime"]["artifact_sha256"], + "IDENTITY_SHA256": image_input["identity"]["bundle_sha256"], + "INPUT_SHA256": image_input["input_sha256"], + "TELEO_REVISION": source_revision, + }, + ) + _safe_tree(output, "prepared image context") + return image_input + except BaseException: + shutil.rmtree(output, ignore_errors=True) + raise + + +def build_release_descriptor(image_input: dict[str, Any], image_reference: str) -> dict[str, Any]: + validate_image_input(image_input) + match = IMAGE_REFERENCE.fullmatch(image_reference) + _require(match is not None, "release image must be the exact staging repository at a sha256 digest") + digest = f"sha256:{match.group(1)}" + labels = expected_image_labels(image_input) + stable = { + "schema": RELEASE_SCHEMA, + "target": _target(), + "image": { + "reference": image_reference, + "digest": digest, + "platform": PLATFORM, + "labels": labels, + "input_sha256": image_input["input_sha256"], + }, + "image_input": image_input, + } + value = {**stable, "release_sha256": canonical_sha256(stable)} + validate_release_descriptor(value) + return value + + +def expected_image_labels(image_input: dict[str, Any]) -> dict[str, str]: + validate_image_input(image_input) + return { + "livingip.artifact.sha256": image_input["runtime"]["artifact_sha256"], + "livingip.identity.sha256": image_input["identity"]["bundle_sha256"], + "livingip.input.sha256": image_input["input_sha256"], + "livingip.runtime": "leoclean-nosend", + "livingip.target": "gcp-staging", + "org.opencontainers.image.revision": image_input["runtime"]["teleo_git_head"], + "org.opencontainers.image.source": "https://github.com/living-ip/teleo-infrastructure", + } + + +def validate_release_descriptor(value: dict[str, Any]) -> None: + _require( + set(value) == {"schema", "target", "image", "image_input", "release_sha256"}, + "release descriptor fields are not exact", + ) + _require(value.get("schema") == RELEASE_SCHEMA, "release descriptor schema is invalid") + _require(value.get("target") == _target(), "release target is not exact GCP staging") + image_input = value.get("image_input") + _require(isinstance(image_input, dict), "release image input is missing") + validate_image_input(image_input) + image = value.get("image") + _require( + isinstance(image, dict) and set(image) == {"reference", "digest", "platform", "labels", "input_sha256"}, + "release image fields are not exact", + ) + reference = image.get("reference") + match = IMAGE_REFERENCE.fullmatch(reference) if isinstance(reference, str) else None + _require(match is not None, "release image is not digest-pinned in the staging repository") + _require(image.get("digest") == f"sha256:{match.group(1)}", "release image digest differs from its reference") + _require(image.get("platform") == PLATFORM, "release platform is not exact") + _require(image.get("input_sha256") == image_input["input_sha256"], "release image input binding drifted") + _require(image.get("labels") == expected_image_labels(image_input), "release image labels are not exact") + stable = {key: item for key, item in value.items() if key != "release_sha256"} + _require(value.get("release_sha256") == canonical_sha256(stable), "release descriptor self-hash drifted") + _assert_no_forbidden_descriptor_markers(value) + + +def render_systemd_unit(release: dict[str, Any]) -> str: + validate_release_descriptor(release) + image = release["image"]["reference"] + release_sha = release["release_sha256"] + profile_tmpfs = f"{PROFILE_ROOT}:rw,noexec,nosuid,nodev,size=64m,uid=0,gid=0,mode=0700" + temp_tmpfs = f"/tmp:rw,noexec,nosuid,nodev,size=64m,uid={RUNTIME_UID},gid={RUNTIME_GID},mode=0700" + postgres_data_tmpfs = "/var/lib/postgresql/data:ro,noexec,nosuid,nodev,size=64k,uid=0,gid=0,mode=0000" + exec_start = " ".join( + [ + "/usr/bin/docker run", + "--rm", + f"--name={CONTAINER_NAME}", + "--pull=never", + "--read-only", + "--cap-drop=ALL", + "--cap-add=CHOWN", + "--cap-add=SETGID", + "--cap-add=SETUID", + "--cap-add=SETPCAP", + "--security-opt=no-new-privileges:true", + "--pids-limit=512", + "--network=bridge", + "--stop-timeout=30", + f"--tmpfs={profile_tmpfs}", + f"--tmpfs={temp_tmpfs}", + f"--tmpfs={postgres_data_tmpfs}", + f"--label=livingip.release.sha256={release_sha}", + image, + "service", + ] + ) + unit = f"""[Unit] +Description=LivingIP leoclean-gcp-nosend staging +Documentation=https://github.com/living-ip/teleo-infrastructure +After=docker.service network-online.target +Requires=docker.service +Wants=network-online.target + +[Service] +Type=simple +ExecStart={exec_start} +ExecStop=/usr/bin/docker stop --time=30 {CONTAINER_NAME} +ExecStopPost=-/usr/bin/docker rm --force {CONTAINER_NAME} +Restart=on-failure +RestartSec=5s +TimeoutStartSec=120s +TimeoutStopSec=45s +UMask=0077 +NoNewPrivileges=yes +CapabilityBoundingSet= +AmbientCapabilities= +PrivateDevices=yes +PrivateTmp=yes +ProtectSystem=strict +ProtectHome=yes +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectKernelLogs=yes +ProtectControlGroups=yes +LockPersonality=yes +RestrictSUIDSGID=yes +RestrictRealtime=yes +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 +SystemCallArchitectures=native + +[Install] +WantedBy=multi-user.target +""" + _require("--publish" not in unit and " -p " not in unit, "service unit publishes a port") + _require("docker.sock" not in unit, "service unit mounts the Docker socket") + _require( + "EnvironmentFile" not in unit and "LoadCredential" not in unit, + "service unit adds an unreviewed credential source", + ) + _require("telegram" not in unit.casefold(), "service unit contains a messaging marker") + return unit + + +def _git_head(repo_root: Path) -> str: + try: + return subprocess.run( + ["git", "-C", str(repo_root), "rev-parse", "HEAD"], + check=True, + capture_output=True, + text=True, + ).stdout.strip() + except (OSError, subprocess.CalledProcessError) as exc: + raise PackageError("cannot determine source revision") from exc + + +def _parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + prepare = subparsers.add_parser("prepare") + prepare.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1]) + prepare.add_argument("--artifact", type=Path, required=True) + prepare.add_argument("--receipt", type=Path, required=True) + prepare.add_argument("--identity", type=Path, required=True) + prepare.add_argument("--output", type=Path, required=True) + finalize = subparsers.add_parser("finalize") + finalize.add_argument("--image-input", type=Path, required=True) + finalize.add_argument("--image-reference", required=True) + finalize.add_argument("--output-release", type=Path, required=True) + finalize.add_argument("--output-unit", type=Path, required=True) + validate = subparsers.add_parser("validate") + validate.add_argument("--release", type=Path, required=True) + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + try: + args = _parse_args(argv) + if args.command == "prepare": + repo_root = args.repo_root.resolve() + result = prepare_context( + repo_root=repo_root, + artifact=args.artifact.resolve(), + receipt=args.receipt.resolve(), + identity=args.identity.resolve(), + output=args.output.absolute(), + source_revision=_git_head(repo_root), + ) + elif args.command == "finalize": + image_input = load_json(args.image_input, "image input") + result = build_release_descriptor(image_input, args.image_reference) + for path in (args.output_release, args.output_unit): + _require(not path.exists() and not path.is_symlink(), "finalize output already exists") + path.parent.mkdir(parents=True, exist_ok=True) + write_json(args.output_release, result) + args.output_unit.write_text(render_systemd_unit(result), encoding="utf-8") + args.output_unit.chmod(0o644) + else: + result = load_json(args.release, "release descriptor") + validate_release_descriptor(result) + print(json.dumps({"status": "pass", "schema": result["schema"]}, sort_keys=True)) + return 0 + except PackageError as exc: + print(json.dumps({"status": "fail", "error": str(exc)}, sort_keys=True), file=sys.stderr) + return 65 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/compile_leoclean_nosend_runtime.py b/scripts/compile_leoclean_nosend_runtime.py index 48b0237..56f2813 100644 --- a/scripts/compile_leoclean_nosend_runtime.py +++ b/scripts/compile_leoclean_nosend_runtime.py @@ -46,11 +46,7 @@ def _canonical_sha256(value: Any) -> str: def _is_sha256(value: Any) -> bool: - return ( - isinstance(value, str) - and len(value) == 64 - and all(character in "0123456789abcdef" for character in value) - ) + return isinstance(value, str) and len(value) == 64 and all(character in "0123456789abcdef" for character in value) def _safe_relative(value: Any, label: str) -> str: @@ -72,7 +68,10 @@ def _load_json(path: Path, label: str) -> dict[str, Any]: def _validate_lock(value: dict[str, Any]) -> None: _require(value.get("schema") == LOCK_SCHEMA, "unsupported runtime lock schema") - _require(set(value) == {"schema", "hermes", "base_images", "patches", "profile_sources"}, "runtime lock fields are invalid") + _require( + set(value) == {"schema", "hermes", "base_images", "patches", "profile_sources"}, + "runtime lock fields are invalid", + ) hermes = value.get("hermes") _require(isinstance(hermes, dict), "Hermes lock is missing") _require( @@ -90,7 +89,10 @@ def _validate_lock(value: dict[str, Any]) -> None: "Hermes lock fields are invalid", ) commit = hermes.get("commit") - _require(isinstance(commit, str) and len(commit) == 40 and all(ch in "0123456789abcdef" for ch in commit), "Hermes commit must be an exact SHA") + _require( + isinstance(commit, str) and len(commit) == 40 and all(ch in "0123456789abcdef" for ch in commit), + "Hermes commit must be an exact SHA", + ) _require(hermes.get("repository") == "https://github.com/NousResearch/hermes-agent", "Hermes repository is invalid") _require( hermes.get("archive_url") == f"https://codeload.github.com/NousResearch/hermes-agent/tar.gz/{commit}", @@ -108,7 +110,10 @@ def _validate_lock(value: dict[str, Any]) -> None: targets: set[str] = set() for patch in patches: _require(isinstance(patch, dict), "patch lock entry must be an object") - _require(set(patch) == {"source", "source_sha256", "target", "before_sha256", "after_sha256"}, "patch lock fields are invalid") + _require( + set(patch) == {"source", "source_sha256", "target", "before_sha256", "after_sha256"}, + "patch lock fields are invalid", + ) _safe_relative(patch["source"], "patch source") target = _safe_relative(patch["target"], "patch target") _require(isinstance(target, str) and target not in targets, "patch targets must be unique") @@ -119,12 +124,15 @@ def _validate_lock(value: dict[str, Any]) -> None: _require(isinstance(sources, list) and bool(sources), "profile sources are missing") profile_targets: set[str] = set() for source in sources: - _require(isinstance(source, dict) and set(source) == {"source", "target", "mode"}, "profile source fields are invalid") + _require( + isinstance(source, dict) and set(source) == {"source", "target", "mode"}, + "profile source fields are invalid", + ) _safe_relative(source["source"], "profile source") target = _safe_relative(source["target"], "profile target") _require(target not in profile_targets, "duplicate profile target") profile_targets.add(target) - _require(source["mode"] in {"0600", "0644", "0755"}, "profile mode is invalid") + _require(source["mode"] in {"0444", "0600", "0644", "0755"}, "profile mode is invalid") def _download(url: str, target: Path, timeout: float) -> None: @@ -190,7 +198,9 @@ def _apply_patches(repo_root: Path, hermes_root: Path, lock: dict[str, Any]) -> patcher = _bound_file(repo_root, entry["source"], f"managed patcher {entry['source']}") target = _bound_file(hermes_root, entry["target"], f"Hermes patch target {entry['target']}") _require(_sha256(patcher) == entry["source_sha256"], f"managed patcher drifted: {entry['source']}") - _require(_sha256(target) == entry["before_sha256"], f"Hermes patch target drifted before apply: {entry['target']}") + _require( + _sha256(target) == entry["before_sha256"], f"Hermes patch target drifted before apply: {entry['target']}" + ) module = _load_patcher(patcher) result, code = module.install(target, check=False) _require(code == 0, f"managed patch failed: {entry['source']}") @@ -219,6 +229,7 @@ def _copy_profile(repo_root: Path, artifact: Path, lock: dict[str, Any]) -> None target.parent.mkdir(parents=True, exist_ok=True) shutil.copyfile(source, target) target.chmod(int(entry["mode"], 8)) + profile.chmod(0o555) def _tree_manifest(root: Path, *, excluded: frozenset[str] = frozenset()) -> dict[str, Any]: @@ -304,8 +315,12 @@ def compile_artifact( extracted = staging / "extracted" extracted.mkdir() hermes_source = _safe_extract(archive_path, extracted, lock["hermes"]["archive_root"]) - _require(_sha256(hermes_source / "pyproject.toml") == lock["hermes"]["pyproject_sha256"], "Hermes pyproject drifted") - _require(_sha256(hermes_source / "uv.lock") == lock["hermes"]["uv_lock_sha256"], "Hermes dependency lock drifted") + _require( + _sha256(hermes_source / "pyproject.toml") == lock["hermes"]["pyproject_sha256"], "Hermes pyproject drifted" + ) + _require( + _sha256(hermes_source / "uv.lock") == lock["hermes"]["uv_lock_sha256"], "Hermes dependency lock drifted" + ) patch_receipts = _apply_patches(repo_root, hermes_source, lock) patched_source = _tree_manifest(hermes_source) _require( diff --git a/scripts/run_leoclean_nosend_runtime_canary.sh b/scripts/run_leoclean_nosend_runtime_canary.sh index 34501ac..09e9e55 100755 --- a/scripts/run_leoclean_nosend_runtime_canary.sh +++ b/scripts/run_leoclean_nosend_runtime_canary.sh @@ -22,6 +22,7 @@ fi temporary="$(mktemp -d "${TMPDIR:-/tmp}/livingip-nosend-runtime.XXXXXX")" cleanup() { + chmod -R u+w "$temporary" 2>/dev/null || true rm -rf "$temporary" } trap cleanup EXIT INT TERM diff --git a/tests/test_gcp_leoclean_nosend_package.py b/tests/test_gcp_leoclean_nosend_package.py new file mode 100644 index 0000000..586de3a --- /dev/null +++ b/tests/test_gcp_leoclean_nosend_package.py @@ -0,0 +1,362 @@ +"""Offline contracts for the isolated GCP leoclean no-send service image.""" + +from __future__ import annotations + +import copy +import json +import os +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +from ops import gcp_leoclean_nosend_package as package + +ROOT = Path(__file__).resolve().parents[1] +REVISION = "1" * 40 +IMAGE_DIGEST = "2" * 64 +CLAIM_CEILING = "Offline package proof only; live GCP identity, database access, restart, and parity remain unproven." + + +def write_json(path: Path, value: object, mode: int = 0o600) -> None: + path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8") + path.chmod(mode) + + +def artifact_fixture(tmp_path: Path) -> tuple[Path, Path]: + artifact = tmp_path / "artifact" + artifact.mkdir() + (artifact / "bootstrap.py").write_text("#!/usr/bin/env python3\n", encoding="utf-8") + (artifact / "bootstrap.py").chmod(0o755) + write_json( + artifact / "runtime-lock.json", + { + "base_images": { + "python": package.PYTHON_IMAGE.removeprefix("docker.io/library/"), + "uv": package.UV_IMAGE, + }, + "hermes": { + "commit": package.HERMES_COMMIT, + "uv_lock_sha256": "3" * 64, + }, + }, + mode=0o644, + ) + profile = artifact / "profile-template" + profile.mkdir() + (profile / "config.yaml").write_text("model: fixture\n", encoding="utf-8") + (profile / "config.yaml").chmod(0o600) + content = package.tree_manifest(artifact) + manifest = { + "schema": package.ARTIFACT_MANIFEST_SCHEMA, + "teleo_git_head": REVISION, + "hermes_commit": package.HERMES_COMMIT, + "claim_ceiling": CLAIM_CEILING, + "content": content, + } + write_json(artifact / "artifact-manifest.json", manifest, mode=0o644) + receipt = tmp_path / "receipt.json" + write_json( + receipt, + { + "schema": package.ARTIFACT_RECEIPT_SCHEMA, + "status": "pass", + "verification_mode": "release", + "teleo_git_head": REVISION, + "artifact_sha256": content["sha256"], + "hermes_commit": package.HERMES_COMMIT, + "hermes_source_sha256": "4" * 64, + "claim_ceiling": CLAIM_CEILING, + }, + mode=0o644, + ) + return artifact, receipt + + +def identity_fixture(tmp_path: Path) -> Path: + identity = tmp_path / "identity" + identity.mkdir(parents=True) + soul = identity / "SOUL.md" + soul.write_text("# Leo fixture\n", encoding="utf-8") + soul.chmod(0o600) + stable = { + "schema": package.IDENTITY_MANIFEST_SCHEMA, + "identity_inputs": { + "canonical_database": { + "authority": "synthetic_noncanonical_fixture", + "canonical_authority_granted": False, + } + }, + "identity_inputs_sha256": "5" * 64, + "compiled_views": { + "SOUL.md": {"sha256": package.sha256_file(soul)}, + "identity.json": {"sha256": "6" * 64}, + }, + } + manifest = {**stable, "manifest_sha256": package.canonical_sha256(stable)} + write_json(identity / "identity-manifest.json", manifest) + write_json( + identity / "identity-lock.json", + { + "schema": package.IDENTITY_LOCK_SCHEMA, + "manifest_sha256": manifest["manifest_sha256"], + "identity_inputs_sha256": manifest["identity_inputs_sha256"], + "compiled_views": { + "SOUL.md": {"sha256": package.sha256_file(soul)}, + "identity.json": {"sha256": "6" * 64}, + }, + "session_boundary": {"classification": "temporary_noncanonical"}, + }, + ) + return identity + + +def image_input_fixture(tmp_path: Path) -> dict[str, object]: + artifact, receipt = artifact_fixture(tmp_path) + identity = identity_fixture(tmp_path) + return package.build_image_input( + artifact=artifact, + receipt=receipt, + identity=identity, + ca=ROOT / "ops" / "gcp-teleo-pgvector-standby-server-ca.pem", + source_revision=REVISION, + dockerfile=ROOT / "docker" / "leoclean-nosend" / "Dockerfile", + entrypoint=ROOT / "docker" / "leoclean-nosend" / "entrypoint.py", + package_contract=ROOT / "ops" / "gcp_leoclean_nosend_package.py", + ) + + +def test_image_input_binds_exact_runtime_identity_and_staging_target(tmp_path: Path) -> None: + value = image_input_fixture(tmp_path) + package.validate_image_input(value) + + assert value["target"] == { + "project": "teleo-501523", + "zone": "europe-west6-a", + "instance": "teleo-staging-1", + "service": "leoclean-gcp-nosend.service", + "service_account": "sa-teleo-staging-vm@teleo-501523.iam.gserviceaccount.com", + "platform": "linux/amd64", + } + assert value["runtime"]["python_version"] == "3.11.9" + assert value["runtime"]["postgres_version"] == "16.14" + assert value["identity"]["canonical_authority_granted"] is False + assert value["base_images"]["postgres"].endswith( + "@sha256:92620daddcd947f8d5ab5ba66e848702fe443d87fed30c4cea8e389fd78dfc55" + ) + + +def test_artifact_requires_release_receipt_exact_revision_and_unchanged_content(tmp_path: Path) -> None: + artifact, receipt = artifact_fixture(tmp_path) + package.validate_artifact(artifact, receipt, REVISION) + + development = json.loads(receipt.read_text(encoding="utf-8")) + development["verification_mode"] = "development" + write_json(receipt, development, mode=0o644) + with pytest.raises(package.PackageError, match="not release"): + package.validate_artifact(artifact, receipt, REVISION) + + development["verification_mode"] = "release" + write_json(receipt, development, mode=0o644) + (artifact / "bootstrap.py").write_text("mutated\n", encoding="utf-8") + (artifact / "bootstrap.py").chmod(0o755) + with pytest.raises(package.PackageError, match="content manifest drifted"): + package.validate_artifact(artifact, receipt, REVISION) + + +def test_identity_bundle_rejects_drift_extra_files_and_false_authority(tmp_path: Path) -> None: + identity = identity_fixture(tmp_path) + package.validate_identity_bundle(identity) + + (identity / "SOUL.md").write_text("drift\n", encoding="utf-8") + (identity / "SOUL.md").chmod(0o600) + with pytest.raises(package.PackageError, match="SOUL content differs"): + package.validate_identity_bundle(identity) + + identity = identity_fixture(tmp_path / "second") + (identity / "extra.txt").write_text("extra\n", encoding="utf-8") + with pytest.raises(package.PackageError, match="entry set"): + package.validate_identity_bundle(identity) + + identity = identity_fixture(tmp_path / "third") + manifest_path = identity / "identity-manifest.json" + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + manifest["identity_inputs"]["canonical_database"] = { + "authority": "synthetic_noncanonical_fixture", + "canonical_authority_granted": True, + } + stable = {key: value for key, value in manifest.items() if key != "manifest_sha256"} + manifest["manifest_sha256"] = package.canonical_sha256(stable) + write_json(manifest_path, manifest) + lock_path = identity / "identity-lock.json" + lock = json.loads(lock_path.read_text(encoding="utf-8")) + lock["manifest_sha256"] = manifest["manifest_sha256"] + write_json(lock_path, lock) + with pytest.raises(package.PackageError, match="authority"): + package.validate_identity_bundle(identity) + + +def test_release_requires_exact_digest_repository_and_strict_schema(tmp_path: Path) -> None: + image_input = image_input_fixture(tmp_path) + reference = f"{package.IMAGE_REPOSITORY}@sha256:{IMAGE_DIGEST}" + release = package.build_release_descriptor(image_input, reference) + package.validate_release_descriptor(release) + + assert release["image"]["reference"] == reference + assert release["image"]["digest"] == f"sha256:{IMAGE_DIGEST}" + assert release["image"]["platform"] == "linux/amd64" + + with pytest.raises(package.PackageError, match="sha256 digest"): + package.build_release_descriptor(image_input, f"{package.IMAGE_REPOSITORY}:latest") + with pytest.raises(package.PackageError, match="staging repository"): + package.build_release_descriptor( + image_input, + f"europe-west6-docker.pkg.dev/teleo-501523/teleo/other@sha256:{IMAGE_DIGEST}", + ) + + extra = copy.deepcopy(release) + extra["unexpected"] = True + with pytest.raises(package.PackageError, match="fields are not exact"): + package.validate_release_descriptor(extra) + + wrong_label = copy.deepcopy(release) + wrong_label["image"]["labels"]["livingip.artifact.sha256"] = "9" * 64 + stable = {key: value for key, value in wrong_label.items() if key != "release_sha256"} + wrong_label["release_sha256"] = package.canonical_sha256(stable) + with pytest.raises(package.PackageError, match="labels are not exact"): + package.validate_release_descriptor(wrong_label) + + +def test_rendered_unit_is_isolated_digest_pinned_and_has_no_credential_or_transport_surface(tmp_path: Path) -> None: + image_input = image_input_fixture(tmp_path) + release = package.build_release_descriptor( + image_input, + f"{package.IMAGE_REPOSITORY}@sha256:{IMAGE_DIGEST}", + ) + unit = package.render_systemd_unit(release) + + assert "leoclean-gcp-nosend" in unit + assert f"@sha256:{IMAGE_DIGEST}" in unit + assert "--pull=never" in unit + assert "--read-only" in unit + assert "--cap-drop=ALL" in unit + assert "--cap-add=CHOWN" in unit + assert "--cap-add=SETGID" in unit + assert "--cap-add=SETUID" in unit + assert "--cap-add=SETPCAP" in unit + assert "--network=bridge" in unit + assert "--tmpfs=/run/leoclean-profile:" in unit + assert "--tmpfs=/var/lib/postgresql/data:ro,noexec,nosuid,nodev,size=64k,uid=0,gid=0,mode=0000" in unit + assert "NoNewPrivileges=yes" in unit + assert "EnvironmentFile" not in unit + assert "LoadCredential" not in unit + assert "docker.sock" not in unit + assert "--publish" not in unit + assert "teleo-prod-1" not in unit + assert "77.42.65.182" not in unit + assert "telegram" not in unit.casefold() + + +def test_prepare_context_is_secret_free_normalized_and_idempotently_refuses_overwrite(tmp_path: Path) -> None: + artifact, receipt = artifact_fixture(tmp_path) + identity = identity_fixture(tmp_path) + output = tmp_path / "context" + result = package.prepare_context( + repo_root=ROOT, + artifact=artifact, + receipt=receipt, + identity=identity, + output=output, + source_revision=REVISION, + ) + + package.validate_image_input(result) + assert sorted(path.name for path in output.iterdir()) == [ + "Dockerfile", + "artifact", + "artifact-receipt.json", + "build-args.json", + "cloudsql-server-ca.pem", + "entrypoint.py", + "identity", + "image-input.json", + "package_contract.py", + ] + assert all((output / "identity" / name).stat().st_mode & 0o777 == 0o600 for name in package.IDENTITY_FILES) + encoded = (output / "image-input.json").read_text(encoding="utf-8").casefold() + assert "telegram_bot_token" not in encoded + assert "postgres-password" not in encoded + + with pytest.raises(package.PackageError, match="already exists"): + package.prepare_context( + repo_root=ROOT, + artifact=artifact, + receipt=receipt, + identity=identity, + output=output, + source_revision=REVISION, + ) + + +def test_dockerfile_has_only_digest_pinned_bases_and_no_floating_package_install() -> None: + dockerfile = (ROOT / "docker" / "leoclean-nosend" / "Dockerfile").read_text(encoding="utf-8") + from_lines = [line for line in dockerfile.splitlines() if line.startswith("FROM ")] + + assert len(from_lines) == 3 + assert all("@sha256:" in line for line in from_lines) + assert "postgres:16.14-bookworm@sha256:92620d" in dockerfile + assert "python:3.11.9-slim-bookworm@sha256:8fb099" in dockerfile + assert "uv:0.9.30@sha256:538e0b" in dockerfile + assert "apt-get" not in dockerfile + assert "pip install" not in dockerfile + assert "uv sync" in dockerfile + assert "--frozen" in dockerfile + assert "--no-editable" in dockerfile + assert "HEALTHCHECK" in dockerfile + assert "verify-build" in dockerfile + + +def test_entrypoint_prepares_exact_profile_drops_privileges_and_health_checks_pid_one() -> None: + source = (ROOT / "docker" / "leoclean-nosend" / "entrypoint.py").read_text(encoding="utf-8") + + assert "os.setgroups([])" in source + assert "os.setgid(contract.RUNTIME_GID)" in source + assert "os.setuid(contract.RUNTIME_UID)" in source + assert "pr_capbset_drop = 24" in source + assert "reverse=True" in source + assert source.index("os.chown(runtime_identity, 0, 0)") < source.index( + "os.chown(PROFILE, contract.RUNTIME_UID, contract.RUNTIME_GID)" + ) + assert 'value.get("process_id") == 1' in source + assert "expected_binfmt = [expected_native[0], *expected_native]" in source + assert "cmdline in (expected_native, expected_binfmt)" in source + assert 'surface.get("send_message_present") is False' in source + assert "gateway_adapter_count" in source + assert "os.execve" in source + assert "OPENROUTER_API_KEY" not in source + assert "TELEGRAM_BOT_TOKEN" not in source + + +def test_entrypoint_loads_adjacent_contract_with_python_safe_path(tmp_path: Path) -> None: + image_root = tmp_path / "image" + image_root.mkdir() + entrypoint = image_root / "entrypoint.py" + shutil.copy2(ROOT / "docker" / "leoclean-nosend" / "entrypoint.py", entrypoint) + shutil.copy2(ROOT / "ops" / "gcp_leoclean_nosend_package.py", image_root / "package_contract.py") + environment = {**os.environ, "PYTHONSAFEPATH": "1"} + + result = subprocess.run( + [sys.executable, "-P", str(entrypoint), "verify"], + cwd=tmp_path, + env=environment, + check=False, + capture_output=True, + text=True, + timeout=10, + ) + + assert result.returncode == 65 + assert json.loads(result.stderr) == {"error": "container_contract_failed", "status": "fail"} + assert "Traceback" not in result.stderr diff --git a/tests/test_leoclean_nosend_runtime.py b/tests/test_leoclean_nosend_runtime.py index 9fc6a74..e4c0faf 100644 --- a/tests/test_leoclean_nosend_runtime.py +++ b/tests/test_leoclean_nosend_runtime.py @@ -7,6 +7,7 @@ import copy import importlib.util import io import json +import os import re import shutil import subprocess @@ -52,6 +53,7 @@ def copy_profile(tmp_path: Path, bootstrap) -> Path: bootstrap._runtime_root = lambda: runtime_root profile = tmp_path / "profile" shutil.copytree(runtime_root / "profile-template", profile) + profile.chmod(0o700) return profile @@ -100,6 +102,19 @@ def test_committed_lock_and_contract_are_exactly_pinned() -> None: } +def test_compiled_immutable_profile_is_readable_after_privilege_drop(tmp_path: Path) -> None: + runtime_root = tmp_path / "runtime" + runtime_root.mkdir() + + compiler._copy_profile(ROOT, runtime_root, LOCK) + + profile = runtime_root / "profile-template" + assert profile.stat().st_mode & 0o777 == 0o555 + assert (profile / "config.yaml").stat().st_mode & 0o777 == 0o444 + assert all(path.stat().st_mode & 0o004 for path in profile.rglob("*") if path.is_file()) + assert all(path.stat().st_mode & 0o001 for path in profile.rglob("*") if path.is_dir()) + + def test_profile_sources_keep_gcp_adapters_out_of_vps_deploy_paths() -> None: by_target = {entry["target"]: entry for entry in LOCK["profile_sources"]} assert by_target["bin/cloudsql_memory_tool.py"]["source"] == ( @@ -219,6 +234,67 @@ def test_profile_template_and_runtime_state_are_exact(tmp_path: Path) -> None: bootstrap._validate_profile(profile, CONTRACT, template=False) +def test_service_readiness_is_atomic_sanitized_and_identity_bound(tmp_path: Path) -> None: + bootstrap = load_bootstrap() + profile = copy_profile(tmp_path, bootstrap) + for name in CONTRACT["profile"]["required_runtime_entries"]: + (profile / name).write_text(f"{name}\n", encoding="utf-8") + state = profile / "state" + state.mkdir() + readiness = state / "readiness.json" + tool_surface = { + "registry_sealed": True, + "send_message_present": False, + "tools": ["skill_view", "skills_list", "terminal"], + } + + bootstrap._write_readiness( + profile, + readiness, + CONTRACT, + SimpleNamespace(adapters={}), + tool_surface, + ) + + payload = json.loads(readiness.read_text(encoding="utf-8")) + assert payload == { + "schema": bootstrap.READINESS_SCHEMA, + "status": "ready", + "process_id": os.getpid(), + "runtime_mode": "gcp_staging_no_send", + "config_sha256": bootstrap._sha256(profile / "config.yaml"), + "gateway_adapter_count": 0, + "connected_platforms": [], + "tool_surface": tool_surface, + "identity_sha256": { + name: bootstrap._sha256(profile / name) + for name in ("SOUL.md", "identity-lock.json", "identity-manifest.json") + }, + } + assert readiness.stat().st_mode & 0o777 == 0o600 + assert str(profile) not in readiness.read_text(encoding="utf-8") + + +def test_service_readiness_rejects_escape_and_symlink(tmp_path: Path) -> None: + bootstrap = load_bootstrap() + profile = copy_profile(tmp_path, bootstrap) + for name in CONTRACT["profile"]["required_runtime_entries"]: + (profile / name).write_text("{}\n", encoding="utf-8") + state = profile / "state" + state.mkdir() + runner = SimpleNamespace(adapters={}) + + with pytest.raises(bootstrap.NoSendContractError, match="inside profile state"): + bootstrap._write_readiness(profile, tmp_path / "readiness.json", CONTRACT, runner, {}) + + target = state / "target.json" + target.write_text("{}\n", encoding="utf-8") + linked = state / "readiness.json" + linked.symlink_to(target) + with pytest.raises(bootstrap.NoSendContractError, match="symlink"): + bootstrap._write_readiness(profile, linked, CONTRACT, runner, {}) + + @pytest.mark.parametrize( ("section", "field", "unsafe"), [ @@ -240,6 +316,7 @@ def test_profile_rejects_learning_policy_drift( bootstrap = load_bootstrap() profile = copy_profile(tmp_path, bootstrap) config_path = profile / "config.yaml" + config_path.chmod(0o600) config = yaml.safe_load(config_path.read_text(encoding="utf-8")) config[section][field] = unsafe config_path.write_text(yaml.safe_dump(config, sort_keys=False), encoding="utf-8") @@ -254,6 +331,7 @@ def test_profile_rejects_smart_model_routing(tmp_path: Path) -> None: bootstrap = load_bootstrap() profile = copy_profile(tmp_path, bootstrap) config_path = profile / "config.yaml" + config_path.chmod(0o600) config = yaml.safe_load(config_path.read_text(encoding="utf-8")) config["smart_model_routing"] = { "enabled": True, @@ -750,6 +828,7 @@ def test_ci_runs_the_release_canary_and_retains_its_receipt() -> None: assert "--development" not in wrapper assert "--allow-dirty" not in wrapper assert 'rm -f "$receipt"' in wrapper + assert 'chmod -R u+w "$temporary"' in wrapper assert 'repeat_artifact="$temporary/artifact-repeat"' in wrapper assert "first != second" in wrapper assert "compile-primary.json" in wrapper