# CD-02 — Pull-based staging planner, dry run Status: LOCAL_DONE / INTEGRATION_PENDING Validation: local PASS; real VPS installation and timer run OWED Scope: staging continuous-deploy plan, Phase 2 only. No rollout, backup, migration, landing switch, VPS, k8s, nginx, production or Phase 3 work. ## Delivered - `infra/deploy/agent/abastevo-deploy-agent` fetches `ghcr.io//abastevo-release:staging`, validates the OCI artifact structure, verifies the manifest layer digest and the cosign keyless certificate identity/issuer, validates `abastevo-release-manifest-v1`, and checks that the release SHA is an ancestor of `main` through the anonymous GitHub compare API. - The agent compares the target release with `/var/lib/abastevo-deploy/current.json` and writes a mode-600 `abastevo-deploy-plan-v1` receipt. It never writes `current.json` and never calls `docker`, `kubectl`, `helm` or `systemctl`. - `infra/deploy/agent/systemd/abastevo-deploy-agent.service` runs as a dedicated user with `flock`, `ProtectSystem=strict`, `NoNewPrivileges=yes`, `PrivateTmp=yes`, a read-only home and a two-minute timer. - `infra/deploy/agent/README.md` documents the only host config file (`/etc/abastevo-deploy/agent.env`, mode 600, never committed), prerequisites, installation, receipt/current schemas and scope limits. No secret, token, host, IP or operator-specific path is committed. - `infra/README.md` links the planner; `make test-deploy-agent` runs the failure-behavior harness. ## Local validation - `make test-deploy-agent` and the Quick gate run the same harness: 33 passed, 0 failed. The harness stubs `oras`, `cosign`, `curl`, `docker`, `kubectl`, `helm` and `systemctl` and covers: new-release planning, no-change planning, tampered/unsigned manifest, foreign identity, commit ahead/diverged, GitHub 404/403/network failure, registry failure, invalid schema, digest mismatch, missing signature layer, invalid current state and missing configuration. Successful cases assert no deployment-tool call and no `current.json` creation. - `shellcheck` and `bash -n` pass for the agent and harness. - Live dry run against the real signed `staging` artifact from run `40498638802` (SHA `44b7b915...`): the agent fetched the public OCI artifact, verified the Sigstore bundle with the exact workflow identity at `refs/heads/main`, confirmed the SHA is reachable from `main`, and wrote a `plan-only` receipt. A second run with a matching `current.json` wrote `no-change`; `current.json` was not modified. - A real tampered manifest with the original bundle was refused by cosign (`invalid signature`) before any receipt was written. A direct `cosign verify-blob` with a foreign workflow identity was refused with `no matching CertificateIdentity found`. - The live checks used only anonymous GHCR/GitHub access and no personal token. ## Outstanding - Install the agent, config and systemd units on the staging host and observe a real timer run. The host is not accessed in this delivery. - Phase 3 owns the schema gate, verified backup, migration job, image rollout, landing switch, smoke checks, rollback and `current.json` updates. - Production remains manual behind G09; no production or release action is included.