# 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:

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```bash
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.
The package opens every component of the explicit Docker config path without
following links and retains that full descriptor chain while Docker runs. Every
component must be root- or current-user-owned and non-writable by group/other;
a root-owned sticky temporary directory is accepted as a boundary only when the
operator directory below it is private. Docker starts in the held final
directory and receives `--config .`, so replacing and restoring the named path
mid-command cannot redirect credential reads. The package also revalidates the
named chain before and after each call while holding an advisory lock on the
final 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.

## Install, restart, verify, and roll back

The installer consumes one finalized release-v3 bundle. Its default invocation
is a dry run: it validates the exact two-file bundle, re-renders the unit from
the descriptor, binds the installer/package/control-helper source closure to
the clean Git revision and this checkout's fixed contract root, captures those
stable bytes once, and prints the proposed target without changing the host:

```bash
python3 ops/install_gcp_leoclean_nosend_release.py \
  --bundle /tmp/leoclean-nosend-release
```

After separate host-restart authorization, the same reviewed revision can be
executed as root:

```bash
sudo python3 ops/install_gcp_leoclean_nosend_release.py \
  --bundle /tmp/leoclean-nosend-release \
  --execute-restart
```

The executable path never pulls an image. Before touching the service it
requires the exact digest reference and normalized config digest already to be
present on the local `unix:///var/run/docker.sock` daemon through an empty,
service-private Docker config directory. The complete executable transaction,
including rollback, holds a nonblocking root-owned mode-`0600` lock at
`/run/leoclean-gcp-nosend-installer.lock`. It installs only the deterministic
unit, a standalone release-bound container-control helper, the exact release
descriptor, and a mode-`0600` install receipt. Atomic publications retain the
published inode identity across durability/readback failure; rollback refuses
to overwrite a pathname that no longer names that inode. The helper controls a
container only through a mode-`0600` CID file in the service's mode-`0700`
runtime directory, then rechecks the full ID, name, immutable image reference,
config digest, release hash, role label, and running state. After the trusted ID
disappears it rechecks that the canonical name is still unbound before removing
the CID file. It never removes a container, uses a mutable name as stop
authority, or invokes `--force`.

An already installed release is verified without a restart. During an upgrade,
the installer accepts only a stable two-readback active/running or inactive/dead
prior posture. Before trusting the old `ExecStop`, it exact-binds effective
`ExecStartPre`, `ExecStart`, `ExecStop`, runtime-directory and systemd security
properties to the release and verifies the supervising PID's executable,
command line and cgroup through `/proc`. It then stops the prior service through
that currently loaded release-bound unit and only afterward replaces and
reloads the unit. Candidate verification also rejects filesystem or effective
systemd drop-ins, environment files, forbidden inherited process environment,
an unexpected fragment, image/config/runtime drift, a non-healthy container,
or any PID, container-ID, invocation-ID, or restart-counter change across the
two readbacks. On failure it stops the candidate, restores the exact prior
files, modes and active/inactive/absent posture, and reloads systemd. An
incomplete rollback fails closed and is reported as such.

This installer does not enable the service, provision IAM, roles or secrets,
contact GCP or Cloud SQL, change Telegram traffic, touch the VPS, approve a
proposal, or promote production. Those remain later, separately authorized
boundaries.

## 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 checked by the separate
`ops/check_gcp_leoclean_nosend_preflight.py` observation receipt. 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.

The installer tests use a disposable filesystem root and exact fake
systemd/Docker readbacks. They prove dry-run non-mutation, local digest/config
binding, first install, no-restart idempotence, old-unit-first upgrade ordering,
effective-environment and later-drop-in rejection, PID/restart-race rejection,
automatic rollback to a running prior release, first-install cleanup, managed
file posture, contract-root source binding, stale-loaded-unit rejection,
supervising-process binding, lock contention, post-rename pathname replacement,
exact inactive-posture rollback, and full-ID/config-digest-only container
control including name-rebind refusal. They do not constitute a live staging
restart or a Cloud SQL identity proof.

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; an authorized installer/restart execution; 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

Release finalization'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. The installer does not delete the bundle, local image, or any
Artifact Registry object. Its bounded rollback restores the managed prior
descriptor, helper, unit, receipt, and service state; it must never restart or
edit the production Leo, Telegram, or VPS services. A live staging execution
and rollback proof remain separately authorized.

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.
