Merge pull request #210 from living-ip/feat/leoclean-build-push-receipt
Some checks are pending
CI / lint-and-test (push) Waiting to run
Some checks are pending
CI / lint-and-test (push) Waiting to run
Bind staging image push to a verified receipt
This commit is contained in:
commit
eb5c60873f
4 changed files with 1907 additions and 258 deletions
|
|
@ -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
|
sources must be tracked regular files whose bytes, modes, staged state, and
|
||||||
SHA-256 values match the exact `HEAD` commit. Unrelated user worktree changes
|
SHA-256 values match the exact `HEAD` commit. Unrelated user worktree changes
|
||||||
remain permitted because they cannot enter this image context.
|
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, push, and receipt one candidate
|
||||||
build itself revalidates that those values equal `image-input.json`.
|
|
||||||
|
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
|
## Finalize one inspected digest-bound release
|
||||||
|
|
||||||
After an authorized build and Artifact Registry push returns the immutable
|
After the authorized build/push command publishes its receipt:
|
||||||
image digest:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ops/gcp_leoclean_nosend_package.py finalize \
|
python3 ops/gcp_leoclean_nosend_package.py finalize \
|
||||||
--image-input /tmp/leoclean-nosend-image-context/image-input.json \
|
--image-input /tmp/leoclean-nosend-image-context/image-input.json \
|
||||||
--image-reference \
|
--build-push-receipt /tmp/livingip-leoclean-receipts/build-push-receipt.json \
|
||||||
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-binary /usr/bin/docker \
|
||||||
--docker-host unix:///var/run/docker.sock \
|
--docker-host unix:///var/run/docker.sock \
|
||||||
--docker-config /home/operator/.docker \
|
--docker-config /home/operator/.docker \
|
||||||
--output-bundle /tmp/leoclean-nosend-release
|
--output-bundle /tmp/leoclean-nosend-release
|
||||||
```
|
```
|
||||||
|
|
||||||
This command uses Docker to pull the exact Artifact Registry digest for
|
This command validates the receipt before Docker, derives the sole
|
||||||
`linux/amd64`, inspects the immutable local image, and refuses to create either
|
`repository@sha256` reference and expected build config from it, independently
|
||||||
bundle file unless the requested repository digest is present and the platform,
|
pulls and inspects that immutable image for `linux/amd64`, and refuses output
|
||||||
config digest, complete required label set, runtime-bearing image configuration,
|
unless the receipt, registry inspection, image input, complete labels, and
|
||||||
image-input hash, and separately supplied reviewed source revision all match.
|
runtime-bearing configuration agree.
|
||||||
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
|
The normalized inspection is embedded in strict release schema
|
||||||
`livingip.leocleanNoSendRelease.v2`; unit generation revalidates it. Release v1
|
`livingip.leocleanNoSendRelease.v3` together with the complete build/push
|
||||||
has no deployed consumer and is intentionally rejected; regenerate any local
|
receipt; unit generation revalidates every cross-binding. Release v1 and v2
|
||||||
v1 artifact. For this Linux-only target, inspection normalizes the deprecated
|
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
|
Windows-only Docker `ArgsEscaped` field while still requiring the exact
|
||||||
entrypoint and command arrays.
|
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.
|
inode and re-fsyncs the parent. Any failure leaves no partial final bundle.
|
||||||
|
|
||||||
The Docker binary, config directory, and local `unix://` daemon endpoint are
|
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.
|
start a container, install a unit, restart a service, or contact Cloud SQL.
|
||||||
Registry authentication and this pull require a separate live-read approval.
|
Registry authentication and this pull require a separate live-read approval.
|
||||||
While holding an advisory lock on the explicit Docker config directory, the
|
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 evidence and live gate
|
||||||
|
|
||||||
Offline tests prove descriptor and inspection strictness, explicit local Docker
|
Offline tests prove strict receipt/descriptor/inspection schemas, exact
|
||||||
authority, non-destructive cache reuse, atomic bundle publication, artifact and
|
prepared-context and reviewed-Git binding, mocked pre/post-push digest
|
||||||
synthetic identity binding,
|
transitions, explicit local Docker authority, identity-safe cache-reference
|
||||||
digest-only images, pinned runtime versions, a secret-free context, the
|
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
|
capability-drop contract, no published ports, no Docker-socket mount, no
|
||||||
environment file, and no production/VPS/Telegram target. CI also builds exactly
|
environment file, and no production/VPS/Telegram target. CI also builds exactly
|
||||||
one `linux/amd64` image, starts it with the reviewed restrictions, requires the
|
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.
|
Pinned base images and build cache may remain.
|
||||||
|
|
||||||
The entrypoint command rejection is not a real Hermes tool invocation, and the
|
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
|
bridge-network smoke does not prove general network-egress denial. The
|
||||||
inspection tests use exact Docker JSON fixtures; they do not prove canonical
|
build/push lifecycle tests use mocked Docker state and the offline inspection
|
||||||
identity, Cloud SQL permissions, or the live Artifact Registry digest contents,
|
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
|
or that `teleo-staging-1` currently has a compatible container
|
||||||
engine, enough disk, the expected architecture, the expected service account,
|
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
|
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
|
stop instead of falling back to the production sync script or a mutable host
|
||||||
virtual environment.
|
virtual environment.
|
||||||
|
|
||||||
Publishing the draft branch and stacked PR is only a review handoff. The next
|
Publishing the branch and stacked PR is only a review handoff. After both the
|
||||||
deployment-evidence step must execute one authorized remote registry read plus
|
digest-inspection package and this receipt producer receive exact-head human
|
||||||
the retained local exact-reference cache mutation against the staging digest.
|
review and merge, separate live gates remain: staging repository/tag-policy
|
||||||
A later installer slice must prove installation and rollback consume the same
|
preflight; authorized build/push receipt production; authorized independent
|
||||||
receipt. Live installation, Artifact Registry publication, service restart,
|
digest finalization; installer/restart/rollback; and Cloud SQL/secret/service
|
||||||
IAM/secret changes, Cloud SQL access, and proposal staging remain separate
|
deployment proof. CI carries no Artifact Registry credentials and performs no
|
||||||
approvals.
|
push. IAM/secret changes, Cloud SQL access, proposal staging, production
|
||||||
|
promotion, Telegram traffic, and VPS actions remain separate authority.
|
||||||
|
|
||||||
## Rollback and sunset
|
## Rollback and sunset
|
||||||
|
|
||||||
This slice's rollback boundary is removal of the unpublished atomic output
|
This slice's automatic rollback removes an unpublished bundle only. Receipt
|
||||||
bundle. The verified repository-digest reference and content-addressable layers
|
pathnames are never removed automatically; the private retained link and any
|
||||||
may remain in the local daemon cache so the finalizer never deletes shared cache
|
ambiguous publication residue require explicit operator inspection. Tool-owned
|
||||||
state it cannot exclusively own. It installs no unit and changes no running
|
build and candidate tags, the verified
|
||||||
service. Restoring a prior descriptor, unit, and digest
|
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
|
belongs to the later installer slice, which must never restart or edit the
|
||||||
production Leo, Telegram, or VPS services.
|
production Leo, Telegram, or VPS services.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,42 +40,61 @@ cut over safely and retire the VPS after soak.
|
||||||
synthetic `linux/amd64` OCI lifecycle proved UID/GID `65532`, zero runtime
|
synthetic `linux/amd64` OCI lifecycle proved UID/GID `65532`, zero runtime
|
||||||
capabilities, no-send health, and complete candidate cleanup on the arm64 Mac
|
capabilities, no-send health, and complete candidate cleanup on the arm64 Mac
|
||||||
Docker daemon. No GCP or live service was contacted.
|
Docker daemon. No GCP or live service was contacted.
|
||||||
- In progress from exact current-main base
|
- PR #206 at exact head `e42d635523fcb90ecdd504b0f0290df7015991e7`
|
||||||
`383996a43beebef57cf84a2ced53213e28a41dca`: make release finalization
|
makes release finalization pull and inspect the exact immutable staging
|
||||||
pull and inspect the exact immutable staging digest and refuse release/unit
|
digest. Its five checks pass, but it must not merge unchanged: PR #209's
|
||||||
output unless the repository binding, `linux/amd64` platform, config digest,
|
final repair at `d2ad4f7c964ff70f01be65ad57290b1346153f95` is the required
|
||||||
complete labels, runtime configuration, input hash, and source revision all
|
corrective layer that prevents finalization from deleting daemon-global
|
||||||
match. Trust-bound JSON is strict, release publication is atomic and
|
digest references a Docker-config lock cannot exclusively own.
|
||||||
no-clobber, and failure rollback re-fsyncs its output parent. This slice does
|
- Published as draft PR #210: one structured, self-hashed build/push receipt
|
||||||
not build, push, install, or deploy an image and has not accessed Artifact
|
producer. It derives the content-bound candidate tag, manifest digest, and
|
||||||
Registry or GCP.
|
config binding from validated pre/post-push daemon state; finalization
|
||||||
- Current gate: finish exact-revision offline tests, then stop for separate
|
consumes that receipt instead of caller-supplied digest/config values. The
|
||||||
branch-publication and human-review approval. Remote registry inspection is
|
branch has been rebuilt directly on PR #209's final repair and carries the
|
||||||
a later, separately authorized live-read gate.
|
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
|
- Not yet live-proven: staging VM/container engine, IAM/Secret Manager, Cloud
|
||||||
SQL effective identity, restart behavior, or model/database parity.
|
SQL effective identity, restart behavior, or model/database parity.
|
||||||
|
|
||||||
## Remaining order
|
## Remaining order
|
||||||
|
|
||||||
1. Finish, publish, and obtain exact-revision human review for immutable image
|
1. Merge PR #209 into PR #206's feature branch, rerun exact-head checks, obtain
|
||||||
inspection and release finalization.
|
human review, and merge the corrected PR #206.
|
||||||
2. Add the structured build/push receipt producer that supplies the independently
|
2. Update, review, retarget, and merge draft PR #210's structured build/push
|
||||||
reviewed manifest and config digests consumed by finalization.
|
receipt producer and receipt-bound release-v3 finalizer.
|
||||||
3. Add the separate fail-closed installer/restart/verification/rollback slice
|
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.
|
that consumes the exact inspection-bound release bundle.
|
||||||
4. Run the read-only Artifact Registry and
|
5. Run the read-only `teleo-staging-1`
|
||||||
`teleo-staging-1` host/engine/service-account preflight.
|
host/engine/service-account preflight.
|
||||||
5. Provision the scoped Cloud SQL role/secret and deploy the separate staging
|
6. Provision the scoped Cloud SQL role/secret and deploy the separate staging
|
||||||
service.
|
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.
|
writes/escalation, receipts, restart stability, and rollback.
|
||||||
7. Run behavioral and database parity backtesting.
|
8. Run behavioral and database parity backtesting.
|
||||||
8. Connect the protected Observatory to canonical claims, proposals, pgvector
|
9. Connect the protected Observatory to canonical claims, proposals, pgvector
|
||||||
projection, and runtime receipts.
|
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
|
## 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
|
service deployment/restart, Telegram change, production promotion, canonical
|
||||||
proposal apply, or VPS access is part of the current offline corrective slice.
|
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
Loading…
Reference in a new issue