teleo-infrastructure/docs/gcp-leoclean-nosend-service-package.md

16 KiB

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 synthetic identity manifest, lock, SOUL.md, structured identity.json, original source hashes, and bundle hashes;
  • the exact no-send model, skills, tool registry, proposal-only database policy, and runtime-probe receipt that produced the identity runtime contract;
  • 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, retaining it only after verification passes:

LEOCLEAN_NOSEND_ARTIFACT_OUTPUT=/tmp/leoclean-nosend-artifact \
  scripts/run_leoclean_nosend_runtime_canary.sh

Derive the identity runtime contract from that artifact and receipt, generate a synthetic/noncanonical manifest from the committed fixture sources, and compile the two generated views:

python3 ops/gcp_leoclean_nosend_package.py identity-contract \
  --artifact /tmp/leoclean-nosend-artifact \
  --receipt .crabbox-results/leoclean-nosend-runtime.json \
  --output /tmp/leoclean-nosend-identity-contract.json

python3 ops/gcp_leoclean_nosend_package.py generate-identity-manifest \
  --artifact /tmp/leoclean-nosend-artifact \
  --receipt .crabbox-results/leoclean-nosend-runtime.json \
  --database-fingerprint fixtures/working-leo/leo-identity-v1/leo-database-fingerprint-v1.json \
  --constitution fixtures/working-leo/leo-identity-v1/leo-constitution-v1.json \
  --database-identity fixtures/working-leo/leo-identity-v1/leo-database-identity-v1.json \
  --identity-source-root . \
  --output /tmp/leoclean-nosend-identity-manifest.json

python3 ops/gcp_leoclean_nosend_package.py compile-identity \
  --artifact /tmp/leoclean-nosend-artifact \
  --receipt .crabbox-results/leoclean-nosend-runtime.json \
  --identity-manifest /tmp/leoclean-nosend-identity-manifest.json \
  --identity-source-root . \
  --output /tmp/leoclean-nosend-identity

The compiled identity directory contains exactly:

  • SOUL.md
  • identity.json
  • identity-lock.json
  • identity-manifest.json

Then run:

python3 ops/gcp_leoclean_nosend_package.py prepare \
  --artifact /tmp/leoclean-nosend-artifact \
  --receipt .crabbox-results/leoclean-nosend-runtime.json \
  --identity /tmp/leoclean-nosend-identity \
  --identity-source-root . \
  --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, source or generated view drift, a runtime/model/skills/permission mismatch, any canonical-authority claim, a changed CA, a non-staging target, or an existing output directory. The resulting context is secret-free. Direct canonical and staging-table writes remain denied; proposal staging remains available only through kb_stage.stage_leoclean_proposal(text,text,text,text,jsonb).

The Git gate is closure-specific: the Dockerfile, entrypoint, package contract, Cloud SQL CA, identity validators, and manifest-derived synthetic identity sources must be tracked regular files whose bytes, modes, staged state, and SHA-256 values match the exact HEAD commit. Unrelated user worktree changes remain permitted because they cannot enter this image context. Prepared directory paths are derived from those bound files, their modes are normalized and revalidated, and empty or otherwise unmanifested directories are rejected before and after the build. Artifact file modes must equal the artifact manifest; identity, copied source/validator, and top-level metadata files use fixed package modes and are revalidated across the same boundary.

Build, push, and receipt one candidate

This is a live Artifact Registry mutation and requires explicit staging-push authorization. Before running it, verify the staging repository's candidate-tag immutability or retention policy. The command deliberately has no caller-supplied repository, tag, build argument, config digest, or manifest digest:

install -d -m 0700 /tmp/livingip-leoclean-receipts

python3 ops/gcp_leoclean_nosend_package.py build-push \
  --repo-root . \
  --image-context /tmp/leoclean-nosend-image-context \
  --docker-binary /usr/bin/docker \
  --docker-host unix:///var/run/docker.sock \
  --docker-config /home/operator/.docker \
  --output-receipt /tmp/livingip-leoclean-receipts/build-push-receipt.json \
  --execute-staging-push

Before contacting Docker, the command revalidates the exact context and binds its source closure to the current clean Git revision. It derives the four build arguments from image-input.json, builds one linux/amd64 image, validates its config/platform/labels/runtime, derives a content-bound candidate tag from the local config digest, and requires zero staging repository digests before push. It does not treat push stdout as authority. A successful push must change the daemon view to exactly one valid staging RepoDigest without changing the image configuration.

The atomically published, no-clobber livingip.leocleanNoSendBuildPushReceipt.v1 receipt binds the source revision, image-input hash, local config/platform/labels/runtime, fixed staging repository, and daemon-observed manifest digest. Its canonical self-hash is an integrity checksum, not a signature, identity attestation, or independent provenance service. Local build, candidate-tag, and digest references are retained because a Docker-config lock does not establish exclusive ownership of daemon-global cache state; the command never issues docker image rm and never uses --force. The remote candidate is also never deleted automatically. If receipt publication fails after a push, those objects remain inert and untrusted until a separately authorized operator action resolves them.

The receipt parent must be an absolute, operator-owned directory without group/other write access. Publication keeps the staged descriptor open, validates the in-memory bytes, hard-links that exact inode without replacement, verifies both names, and fsyncs the directory. The private staged hard link is retained intentionally: portable Unix interfaces cannot conditionally unlink a pathname only if it still names a previously verified inode. Publication therefore never deletes a receipt pathname automatically, and ambiguous failure residue is left for explicit operator inspection. The receipt is mode 0600, has exactly two links, and is consumed later through the same private directory and stable-inode contract.

Finalize one inspected digest-bound release

After the authorized build/push command publishes its receipt:

python3 ops/gcp_leoclean_nosend_package.py finalize \
  --image-input /tmp/leoclean-nosend-image-context/image-input.json \
  --build-push-receipt /tmp/livingip-leoclean-receipts/build-push-receipt.json \
  --docker-binary /usr/bin/docker \
  --docker-host unix:///var/run/docker.sock \
  --docker-config /home/operator/.docker \
  --output-bundle /tmp/leoclean-nosend-release

This command validates the receipt before Docker, derives the sole repository@sha256 reference and expected build config from it, independently pulls and inspects that immutable image for linux/amd64, and refuses output unless the receipt, registry inspection, image input, complete labels, and runtime-bearing configuration agree. The normalized inspection is embedded in strict release schema livingip.leocleanNoSendRelease.v3 together with the complete build/push receipt; unit generation revalidates every cross-binding. Release v1 and v2 have no deployed consumer and are intentionally rejected; regenerate any local artifact. For this Linux-only target, inspection normalizes the deprecated Windows-only Docker ArgsEscaped field while still requiring the exact entrypoint and command arrays.

The output parent must already exist as an absolute, non-symlink directory. The output is one atomically published directory containing release.json and leoclean-gcp-nosend.service. Both files are rendered, strictly JSON-decoded, revalidated, and fsynced in a sibling temporary directory before a platform-native no-replace directory rename. An existing or concurrently created destination is never overwritten. The staged directory and parent are fsynced; a post-publication durability failure removes only the same published inode and re-fsyncs the parent. Any failure leaves no partial final bundle.

The Docker binary, config directory, and local unix:// daemon endpoint are explicit inputs; inherited DOCKER_*, BUILDKIT_*, and BUILDX_* overrides are removed and Docker stdin is closed. The pull does not start a container, install a unit, restart a service, or contact Cloud SQL. Registry authentication and this pull require a separate live-read approval. While holding an advisory lock on the explicit Docker config directory, the command pulls and records the dynamically observed config ID. It intentionally retains the exact repository-digest reference: Docker's image cache belongs to the daemon, so a config-directory lock cannot prove that another client did not start relying on the same immutable reference after the precheck. Repeated runs re-pull and re-inspect the same digest without deleting shared cache state. Content-addressable layers and the verified digest reference may remain. A failed pull or inspection returns a bounded error without forwarding Docker stderr.

Tags, alternate repositories, alternate projects/instances/service accounts, extra descriptor fields, wrong-architecture images, missing repository-digest bindings, mismatched image labels, duplicate JSON keys, and non-finite JSON values are rejected.

Offline evidence and live gate

Offline tests prove strict receipt/descriptor/inspection schemas, exact prepared-context and reviewed-Git binding, mocked pre/post-push digest transitions, explicit local Docker authority, identity-safe cache-reference handling with retained immutable digest references, atomic receipt and bundle publication, artifact and synthetic 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. CI also builds exactly one linux/amd64 image, starts it with the reviewed restrictions, requires the built-in zero-adapter/no-send_message health response, checks that an unsupported send-message entrypoint command fails with the exact redacted error, and binds the running container to the dynamically resolved image ID. Before startup, the smoke also requires the built image to declare the implicit root bootstrap user, exact Python entrypoint, service command, healthcheck timings, and SIGTERM stop signal. The successful health command then proves PID 1 dropped to UID/GID 65532 with zero Linux capabilities. Finally, the smoke removes the candidate container, tag, and visible image ID. The receipt records whether linux/amd64 ran natively or through cross-platform emulation. Pinned base images and build cache may remain.

The entrypoint command rejection is not a real Hermes tool invocation, and the bridge-network smoke does not prove general network-egress denial. The build/push lifecycle tests use mocked Docker state and the offline inspection tests use exact Docker JSON fixtures; they do not prove canonical identity, Cloud SQL permissions, a real Artifact Registry push, the registry's tag policy, or the live Artifact Registry digest contents, or 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 branch and stacked PR is only a review handoff. After both the digest-inspection package and this receipt producer receive exact-head human review and merge, separate live gates remain: staging repository/tag-policy preflight; authorized build/push receipt production; authorized independent digest finalization; installer/restart/rollback; and Cloud SQL/secret/service deployment proof. CI carries no Artifact Registry credentials and performs no push. IAM/secret changes, Cloud SQL access, proposal staging, production promotion, Telegram traffic, and VPS actions remain separate authority.

Rollback and sunset

This slice's automatic rollback removes an unpublished bundle only. Receipt pathnames are never removed automatically; the private retained link and any ambiguous publication residue require explicit operator inspection. Tool-owned build and candidate tags, the verified repository-digest reference, and content-addressable layers may remain because a Docker-config lock cannot establish exclusive ownership of daemon-global cache state. A remotely pushed candidate is intentionally not deleted during automatic rollback and cannot be consumed without a valid receipt plus independent finalization. Any later cache cleanup requires separately proven daemon isolation. This slice installs no unit and changes no running service. Restoring a prior descriptor, unit, and digest belongs to the later installer slice, which 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.