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

258 lines
13 KiB
Markdown

# 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.
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 inspected 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> \
--expected-revision <reviewed-40-hex-git-revision> \
--expected-input-sha256 <reviewed-image-input-sha256> \
--expected-config-digest sha256:<reviewed-build-config-digest> \
--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 uses Docker to pull the exact Artifact Registry digest for
`linux/amd64`, inspects the immutable local image, and refuses to create either
bundle file unless the requested repository digest is present and the platform,
config digest, complete required label set, runtime-bearing image configuration,
image-input hash, and separately supplied reviewed source revision all match.
The config digest is a separate, manually reviewed trust input from the
authorized build output; copying it from this command's own registry inspection
would defeat the independent binding. This repository does not yet produce a
structured build-receipt artifact, so this slice must not be described as
proving build-receipt provenance. That producer belongs to the separately
reviewed build/push slice before live finalization.
The normalized inspection is embedded in strict release schema
`livingip.leocleanNoSendRelease.v2`; unit generation revalidates it. Release v1
has no deployed consumer and is intentionally rejected; regenerate any local
v1 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_*` overrides are removed. 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 descriptor and inspection strictness, explicit local Docker
authority, non-destructive cache reuse, atomic 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 offline
inspection tests use exact Docker JSON fixtures; they do not prove canonical
identity, Cloud SQL permissions, 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 draft branch and stacked PR is only a review handoff. The next
deployment-evidence step must execute one authorized remote registry read plus
the retained local exact-reference cache mutation against the staging digest.
A later installer slice must prove installation and rollback consume the same
receipt. Live installation, Artifact Registry publication, service restart,
IAM/secret changes, Cloud SQL access, and proposal staging remain separate
approvals.
## Rollback and sunset
This slice's rollback boundary is removal of the unpublished atomic output
bundle. The verified repository-digest reference and content-addressable layers
may remain in the local daemon cache so the finalizer never deletes shared cache
state it cannot exclusively own. It 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.