Skip to content

fix(release): bind package and manifest coherence (010 draft, stacked 007-010)#128

Open
donbeave wants to merge 4 commits into
mainfrom
runtime/010-package-coherence-draft
Open

fix(release): bind package and manifest coherence (010 draft, stacked 007-010)#128
donbeave wants to merge 4 commits into
mainfrom
runtime/010-package-coherence-draft

Conversation

@donbeave

Copy link
Copy Markdown
Collaborator

STACKED DRAFT PR — base main currently bundles 4 plans. The 007/008/009
base branches are not yet on the remote, so this PR against main contains
commits d6b51e6 (007), 281f2dd (008), 67bb59e (009), 3a7f03f (010).
Only the top commit is plan 010. Rebase onto the 009 base once it lands to get
a 010-only diff. Nothing here is deployed; no push to velnor-apt, no B17/sentry.

Plan 010 — bind Velnor package and manifest coherence

One release commit binds, in one acyclic chain: source SHA → crate version →
per-arch binary/deb digests → OCI image digest → compiled-manifest hash → APT
coordinate → deployed export.

Rust core (offline-testable)

  • build.rs + release-build feature: embeds the 40-hex source SHA + v*
    tag; build fails unless tag == crate version == Cargo.lock on a clean,
    unambiguous tree. Default builds stamp development and cannot emit a record.
  • src/release.rs (+ src/release/tests.rs): validated
    SourceSha/Sha256Hex/OciDigest newtypes;
    BuildIdentity/ArchitectureIdentity/ReleaseRecord/PublicationRecord/DeployedIdentity
    serde schemas (deny_unknown_fields); deterministic canonical emit + digest
    (the record never contains its own digest — acyclic);
    verify/verify_record_bytes/verify_installed/verify_publication_binds
    map every single-field defect to a distinct redacted error; atomic
    fsync+rename activate/rollback with no-clobber.
  • cli.rs/main.rs: release emit|assemble|verify-record|verify-installed|activate|rollback|export.
  • manifest.rs v6 → v7: export adds source_sha + crate_version; 009
    integrity checks intact.

Drafted infra (not executed)

  • release.yml is the sole tag coordinator: peel/ambiguity/non-HEAD/reused/
    dirty/crate-drift gate; one multi-platform GHCR image with
    version/revision/source/manifest-hash labels + captured digests; per-arch
    tar/deb under release-build with the folded empty-deb guards; assemble +
    re-verify one record; create-once/no-clobber. release-deb.yml deleted;
    GH_VELNOR_APT_TOKEN removed (N6).
  • debian/preinst (new) refuses an upgrade unless daemons are drained and a
    prior rollback tuple exists; postinst never builds/never restarts;
    both .service units run release verify-installed as ExecStartPre.

Gates

fmt / clippy -D warnings / actionlint / git diff --check clean; nextest
790 passed
.

Divergences to reconcile before real execution

  1. Crate version is still 0.1.58 while tags are 0.1.121 — a real tagged
    release-build fails the coherence gate until crate + Cargo.lock are
    bumped (the gate working as designed).
  2. verify-installed ExecStartPre is a hard gate: a plain apt install +
    systemctl start without a staged coherent record will not start.
  3. First migration needs an operator-provisioned legacy-observed bootstrap
    tuple from B17 (not synthesized offline).
  4. The record is not shipped inside the deb (circular — it holds the deb digest);
    the deb ships only acyclic build-identity.json + manifest.json, staged
    out-of-band by release activate.
  5. Live OCI label/digest verification is behind a flag (needs a live registry).
  6. Untested against a real tagged build / live GHCR / live APT / sentry.

🤖 Generated with Claude Code

donbeave and others added 4 commits July 24, 2026 17:25
OFFLINE DRAFT for plan 007 — code + tests only, not deployed/pushed.

Carry source_path and derive CacheActionKind {Root,Restore,Save} in the
native invocation; dispatch phases and outputs by kind (root restore+post,
restore-only, save-only with no prior restore); scope cache entries by a
deterministic version segment H(action_ref || RUNNER_OS || RUNNER_ARCH ||
normalized declared paths) below <trust>/caches/<repo>; replace the save
string-sniff with an explicit SaveOutcome enum plus lock-wait/copy/total
timing.

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
OFFLINE DRAFT for plan 008 — code + tests only, not deployed/pushed. Builds
on the 007 cache draft.

Strict fail-closed mise policy (new crate::mise): exact YYYY.M.D version rule
(omitted -> fleet pin 2026.7.7, no live 'latest' lookup; 008-R5), install_args
constrained to tool keys committed in the adjacent lock, and locked-config
resolution requiring a tracked+clean adjacent lock with [settings] lockfile=true
(toml-parsed, never regex). Wire the version+install_args shape rules into the
capability manifest via InputRule::Predicate; gate native_mise install fail-
closed before any command.

Persistent per-version mise binary store mise/binaries/<trust>/<repo>/<os-arch>/
<version>/mise (+metadata.json), mounted at /opt/velnor/mise-binaries, added to
leases and the mise GC budget. setup_mise_script now copies the read-only baked
/opt/mise/bin bootstrap to same-fs staging, self-updates to the exact version
with project config disabled, verifies version+SHA-256, atomically publishes
0555, and reuses on version/hash match (corruption fails closed).

Locked installs only: mise install --locked --yes under MISE_LOCKED/
MISE_LOCKED_VERIFY_PROVENANCE; delete the mise.run bootstrap and the cosign
curl / mold apt / just apt+cargo fallbacks — mold/just/cosign become admitted
locked mise keys verified by version. Add docker/job-mise.{toml,lock}
(linux-x64/arm64) and rewrite the Dockerfile to install the whole toolchain
fail-closed; add lockfile=true to root mise.toml.

Add toml workspace dep. Tests: mise policy unit tests; a shell-level reuse
test (isolated stores + fake updater proving one publish, identical hash/path,
two locked installs, zero prompts, no bootstrap write) + corruption fail-closed;
manifest/container/cache coverage; updated the mise-self-update, curl/apt 5-exec
integration, mold, just and cosign script tests to the new locked contract.

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Replace the split validation boundary (flat top-level check + local-only
recursive preflight + redundant post-download re-resolution, all sharing one
bypass switch) with a single typed, read-only admission graph completed before
any step side effect.

- manifest.rs (v6): structurally immutable identities. assert_manifest_integrity()
  at startup rejects any non-__native ref that is neither a full 40-hex SHA nor an
  explicit plan-041 fixture-transition tag, plus duplicate identities and unsafe
  subpaths. Adds action subpaths (actions/cache: root/restore/save) and a
  reusable-workflow schema; validate_resolved_action now takes source_path.
- admission.rs (new): ActionIdentity/AdmissionNode/Edge/Ancestry, injectable
  ActionMetadataSource (Contents API in prod, fake in tests). admit_job() resolves
  and validates every root (local and remote), recurses nested local+remote,
  resolves defaults and ${{ inputs.* }} before child validation, bounds
  depth/nodes, guards cycles, redacts received values, and admits the one approved
  reusable workflow by repository/path/full-SHA and dispatched inputs.
- runner.rs: admit at the gate before lease renewal/leases/checkout/download;
  planning consumes the graph and never re-resolves; delete the local-only
  preflight and the post-download re-resolution; add side-effect counters.
- cli.rs/main.rs/debian: delete skip_capability_validation,
  diagnostic_node_sidecar, allow_unknown_action_diagnostics and the unknown-JS
  execution branch (keep the hard bail); pre-dispatch strict-env enforcement;
  both .service units pin VELNOR_CAPABILITY_VALIDATION=strict; document only
  strict in velnor.env and the strict-capability contract.

Draft only: not pushed, not deployed, no B17/sentry changes.

Co-authored-by: Codex <codex@openai.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
OFFLINE DRAFT for plan 010 — code + tests + drafted workflow/host-script
files only. Not pushed, not deployed; no B17/sentry/host mutation, no APT
publication. Builds on the 007/008/009 runtime drafts.

Non-circular release identity model (new src/release.rs, mod release):
- build.rs + `release-build` feature embed the exact 40-hex source SHA and
  `v*` tag; the build fails unless tag == crate version == Cargo.lock and the
  tree is clean/unambiguous. Default (feature-off) builds stamp `development`
  and cannot emit a publishable record (proven by unit test).
- Canonical serde schemas BuildIdentity/ArchitectureIdentity/ReleaseRecord/
  PublicationRecord/DeployedIdentity with validated SourceSha/Sha256Hex/
  OciDigest newtypes (40/64-hex, sha256: prefix) and deny_unknown_fields.
  Deterministic canonical emit + digest; the record never contains its own
  digest (acyclic — the digest lives in the checksum sidecar, publication
  record, and deployed pointer). verify()/verify_record_bytes()/
  verify_installed()/verify_publication_binds() map every single-field defect
  to a distinct, value-redacted CoherenceError. Atomic fsync+rename ReleaseStore
  activate/rollback keeps the exact prior tuple; no-clobber on divergent bytes.
- New `release emit|assemble|verify-record|verify-installed|activate|rollback|
  export` CLI (cli.rs + main.rs). `capabilities export` gains the embedded
  source SHA + crate version.

Manifest v6 -> v7: ExportManifest adds source_sha + crate_version identity;
009's structural integrity checks are unchanged. Version assertions updated.

Single producer workflow (drafted, not run): release.yml is the sole tag
coordinator — peels the tag to one commit; rejects ambiguity/non-HEAD/reused-
tag/dirty/crate-tag drift; builds one multi-platform GHCR image with version/
revision/source/manifest-hash labels + captured digests; builds per-arch
tar/deb under release-build with the folded empty-deb guards (binary present,
canonical Dockerfile present, size>1MB, arch/version match); assembles + re-
verifies ONE release record; creates the release once, accepting an existing
tag only on exact digest match (never --clobber). release-deb.yml is deleted
and the source holds NO APT credential (removes GH_VELNOR_APT_TOKEN — N6).

Transactional host activation (drafted): debian/preinst refuses an upgrade
unless daemons are drained and an exact prior rollback tuple exists; postinst
never builds/never restarts — it validates installed binary/package/manifest
identity offline and stages; both .service units run `release verify-installed`
as ExecStartPre so a mixed old/new tuple never starts.

Tests: determinism, malformed SHA/digest/version, crate/tag drift, per-arch
completeness, every single-field mismatch, missing checksum/record, non-
canonical bytes, OCI-label mismatch, publication binding, coherent rollback,
redacted diagnostics — all from fixed fixture bytes (no real tagged build).
fmt + clippy(-D warnings) + actionlint clean; nextest 790 passed.

Co-authored-by: Codex <codex@openai.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant