Bind staging image push to a verified receipt

This commit is contained in:
fwazb 2026-07-20 21:30:41 -07:00
parent d2ad4f7c96
commit ac4bf5f01f
4 changed files with 1907 additions and 258 deletions

View file

@ -132,44 +132,88 @@ 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.
Use the exact values in `build-args.json` as Docker build arguments. The image
build itself revalidates that those values equal `image-input.json`.
## 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 an authorized build and Artifact Registry push returns the immutable
image digest:
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 \
--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> \
--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 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.
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.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
`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.
@ -183,7 +227,8 @@ 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
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
@ -203,10 +248,11 @@ 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
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
@ -222,9 +268,11 @@ 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,
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
@ -232,21 +280,28 @@ 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.
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 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
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.

View file

@ -40,42 +40,61 @@ cut over safely and retire the VPS after soak.
synthetic `linux/amd64` OCI lifecycle proved UID/GID `65532`, zero runtime
capabilities, no-send health, and complete candidate cleanup on the arm64 Mac
Docker daemon. No GCP or live service was contacted.
- In progress from exact current-main base
`383996a43beebef57cf84a2ced53213e28a41dca`: make release finalization
pull and inspect the exact immutable staging digest and refuse release/unit
output unless the repository binding, `linux/amd64` platform, config digest,
complete labels, runtime configuration, input hash, and source revision all
match. Trust-bound JSON is strict, release publication is atomic and
no-clobber, and failure rollback re-fsyncs its output parent. This slice does
not build, push, install, or deploy an image and has not accessed Artifact
Registry or GCP.
- Current gate: finish exact-revision offline tests, then stop for separate
branch-publication and human-review approval. Remote registry inspection is
a later, separately authorized live-read gate.
- PR #206 at exact head `e42d635523fcb90ecdd504b0f0290df7015991e7`
makes release finalization pull and inspect the exact immutable staging
digest. Its five checks pass, but it must not merge unchanged: PR #209's
final repair at `d2ad4f7c964ff70f01be65ad57290b1346153f95` is the required
corrective layer that prevents finalization from deleting daemon-global
digest references a Docker-config lock cannot exclusively own.
- Published as draft PR #210: one structured, self-hashed build/push receipt
producer. It derives the content-bound candidate tag, manifest digest, and
config binding from validated pre/post-push daemon state; finalization
consumes that receipt instead of caller-supplied digest/config values. The
branch has been rebuilt directly on PR #209's final repair and carries the
same retained-digest rule through both build/push and finalization. No
daemon-global build tag, candidate tag, immutable digest reference, or
receipt pathname is removed automatically. The receipt is published and
consumed only through a private operator-owned directory with a retained
two-link, stable-inode contract; remote candidates are retained. The exact
#210 head belongs in the mutable PR evidence, not recursively inside its own
commit.
- Current exact-head validation: 206 focused package/OCI tests and the full
repository suite at 2,480 passed with 3 expected skips; Ruff, Python compile,
and diff checks pass. Two independent advisory exact-head reviews approved
the receipt deletion/identity boundary. Tests mock Docker and perform no
registry push.
- Current gate: publish and review the final #209-based #210 head, allow PR
#209 to correct PR #206's feature branch, rerun/review corrected #206, and
only then merge/retarget the narrow stack. Registry access remains a later,
explicitly authorized live 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. Finish, publish, and obtain exact-revision human review for immutable image
inspection and release finalization.
2. Add the structured build/push receipt producer that supplies the independently
reviewed manifest and config digests consumed by finalization.
3. Add the separate fail-closed installer/restart/verification/rollback slice
1. Merge PR #209 into PR #206's feature branch, rerun exact-head checks, obtain
human review, and merge the corrected PR #206.
2. Update, review, retarget, and merge draft PR #210's structured build/push
receipt producer and receipt-bound release-v3 finalizer.
3. Under explicit live authorization, verify the staging repository/tag policy,
build and push one candidate, publish its receipt, and independently finalize
the exact digest.
4. Add the separate fail-closed installer/restart/verification/rollback slice
that consumes the exact inspection-bound release bundle.
4. Run the read-only Artifact Registry and
`teleo-staging-1` host/engine/service-account preflight.
5. Provision the scoped Cloud SQL role/secret and deploy the separate staging
5. Run the read-only `teleo-staging-1`
host/engine/service-account preflight.
6. Provision the scoped Cloud SQL role/secret and deploy the separate staging
service.
6. Prove effective identity, reads, function-only proposal staging, denied
7. Prove effective identity, reads, function-only proposal staging, denied
writes/escalation, receipts, restart stability, and rollback.
7. Run behavioral and database parity backtesting.
8. Connect the protected Observatory to canonical claims, proposals, pgvector
8. Run behavioral and database parity backtesting.
9. Connect the protected Observatory to canonical claims, proposals, pgvector
projection, and runtime receipts.
9. Cut over, soak, restore-test, revoke VPS credentials, and retire the VPS.
10. Cut over, soak, restore-test, revoke VPS credentials, and retire the VPS.
## Current live-action boundary
No merge, Artifact Registry push, GCP/Cloud SQL/Secret Manager mutation,
No merge without explicit human approval, 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 corrective slice.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff