carlos / platform Public

zdd-4 task 4: CanonicalDemand reads the canonical manifest and checks the staged digest

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 44e0570573e27454c9c049ac4a7b5300f1455ad4 parent acf3bd9
2 files changed, +176 −0
  • internal/release/demand.go +48 −0
  • internal/release/demand_test.go +128 −0
diff --git a/internal/release/demand.go b/internal/release/demand.go
index b452c6fb9..cb5139422 100644
--- a/internal/release/demand.go
+++ b/internal/release/demand.go
@@ -2,6 +2,13 @@
// awake instance may move onto a build, as the build's shipper said.
package release
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+)
+
// DemandHold is the one swap demand a release can make (`carlos deploy
// --swap hold`, spec §9.1).
const DemandHold = "hold"
@@ -16,3 +23,44 @@ func NormalizeDemand(swap string) string {
}
return DemandHold
}
+
+// ErrStagedDigestMismatch is an adoption attempt's staging error (spec §4.2):
+// the staged executable is not the artifact the canonical manifest names for
+// this platform, so the manifest's demand does not describe the bytes that
+// would run. The attempt is refused, with no failure record against the
+// build.
+var ErrStagedDigestMismatch = errors.New("staged artifact is not the release's canonical artifact")
+
+// CanonicalDemand reads build version's own swap demand from its CANONICAL
+// manifest in the release store, and checks the staged executable at
+// stagedPath against that manifest's digest for platform (spec §4.2).
+//
+// - Match: the manifest's demand (NormalizeDemand), the staged digest, nil.
+// - Mismatch: "", the staged digest, an error wrapping
+// ErrStagedDigestMismatch.
+// - Cannot compare (the manifest is unreadable, it has no artifact for
+// platform, or the staged file cannot be read): DemandHold and a nil
+// error, because missing or unverifiable policy data never permits an
+// overlap. digest is the staged file's when it could be hashed, else "".
+//
+// It is for ADOPTION attempts only (causes release, install, retry). Recovery
+// checks a retained digest with VerifyStaged and never asks the store.
+func CanonicalDemand(ctx context.Context, s Store, acct, app, version, platform, stagedPath string) (demand string, digest string, err error) {
+ digest, herr := FileSHA256(stagedPath)
+ if herr != nil {
+ return DemandHold, "", nil
+ }
+ m, _, merr := GetManifestRaw(ctx, s, acct, app, version)
+ if merr != nil {
+ return DemandHold, digest, nil
+ }
+ art, ok := m.Artifacts[platform]
+ if !ok {
+ return DemandHold, digest, nil
+ }
+ if !strings.EqualFold(art.SHA256, digest) {
+ return "", digest, fmt.Errorf("%w: %s/%s staged at %s hashes to %s; the release names %s",
+ ErrStagedDigestMismatch, app, version, stagedPath, digest, art.SHA256)
+ }
+ return NormalizeDemand(m.Swap), digest, nil
+}
diff --git a/internal/release/demand_test.go b/internal/release/demand_test.go
index 75510154b..bf8bc6ca0 100644
--- a/internal/release/demand_test.go
+++ b/internal/release/demand_test.go
@@ -4,7 +4,9 @@ import (
"bytes"
"context"
"encoding/json"
+ "errors"
"fmt"
+ "os"
"strings"
"testing"
)
@@ -100,3 +102,129 @@ func TestNormalizeDemand(t *testing.T) {
}
}
}
+
+// unreadableStore answers every Get with an error that is not "not found": a
+// bucket this box cannot read right now.
+type unreadableStore struct{ Store }
+
+func (unreadableStore) Get(context.Context, string) ([]byte, string, error) {
+ return nil, "", errors.New("bucket unreachable")
+}
+
+// Spec §12 "Swap mode source": a staged copy without swap, from an older
+// root, cannot produce an overlap for a hold build.
+func TestCanonicalDemandReadsTheCanonicalManifestNotTheSibling(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ opt := t.TempDir()
+ putRawRelease(t, s, "bab", "km", "v1", []byte("bin"), `"swap":"hold",`)
+ if err := StageArtifact(opt, "bab", "v1", "km", []byte("bin")); err != nil {
+ t.Fatal(err)
+ }
+ canon, err := GetManifest(ctx, s, "bab", "km", "v1")
+ if err != nil {
+ t.Fatal(err)
+ }
+ lossy := *canon
+ lossy.Swap = "" // what an older root's re-marshal wrote
+ b, err := json.MarshalIndent(lossy, "", " ")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := StageManifest(opt, "bab", "v1", "km", b); err != nil {
+ t.Fatal(err)
+ }
+ demand, digest, err := CanonicalDemand(ctx, s, "bab", "km", "v1", adoptPlatform, VersionPath(opt, "bab", "v1", "km"))
+ if err != nil || demand != DemandHold || digest != etagOf([]byte("bin")) {
+ t.Fatalf("CanonicalDemand = %q, %q, %v; want hold from the canonical manifest", demand, digest, err)
+ }
+}
+
+func TestCanonicalDemandWithNoDemand(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ opt := t.TempDir()
+ putRawRelease(t, s, "bab", "km", "v1", []byte("bin"), "")
+ if err := StageArtifact(opt, "bab", "v1", "km", []byte("bin")); err != nil {
+ t.Fatal(err)
+ }
+ demand, digest, err := CanonicalDemand(context.Background(), s, "bab", "km", "v1", adoptPlatform, VersionPath(opt, "bab", "v1", "km"))
+ if err != nil || demand != "" || digest != etagOf([]byte("bin")) {
+ t.Fatalf("CanonicalDemand = %q, %q, %v; want no demand", demand, digest, err)
+ }
+}
+
+// Spec §12 "Artifact binding": a re-shipped name with different bytes, plus
+// an old staged copy, is a staging error.
+func TestCanonicalDemandRefusesBytesTheReleaseDoesNotName(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ opt := t.TempDir()
+ putRawRelease(t, s, "bab", "km", "v1", []byte("new bytes"), "")
+ if err := StageArtifact(opt, "bab", "v1", "km", []byte("old bytes")); err != nil {
+ t.Fatal(err)
+ }
+ demand, digest, err := CanonicalDemand(context.Background(), s, "bab", "km", "v1", adoptPlatform, VersionPath(opt, "bab", "v1", "km"))
+ if !errors.Is(err, ErrStagedDigestMismatch) {
+ t.Fatalf("err = %v, want ErrStagedDigestMismatch", err)
+ }
+ if demand != "" || digest != etagOf([]byte("old bytes")) {
+ t.Fatalf("CanonicalDemand = %q, %q; want no demand and the staged file's own digest", demand, digest)
+ }
+}
+
+// Spec §4.2: if the canonical manifest cannot be read, or the digests cannot
+// be compared, the attempt runs in hold. Review Focus 2 is the last case.
+func TestCanonicalDemandIsHoldWhenItCannotCompare(t *testing.T) {
+ ctx := context.Background()
+ staged := func(t *testing.T, opt string) string {
+ if err := StageArtifact(opt, "bab", "v1", "km", []byte("bin")); err != nil {
+ t.Fatal(err)
+ }
+ return VersionPath(opt, "bab", "v1", "km")
+ }
+ t.Run("manifest missing", func(t *testing.T) {
+ opt := t.TempDir()
+ d, sum, err := CanonicalDemand(ctx, NewDirStore(t.TempDir()), "bab", "km", "v1", adoptPlatform, staged(t, opt))
+ if err != nil || d != DemandHold || sum != etagOf([]byte("bin")) {
+ t.Fatalf("= %q, %q, %v; want hold", d, sum, err)
+ }
+ })
+ t.Run("store unreadable", func(t *testing.T) {
+ opt := t.TempDir()
+ s := NewDirStore(t.TempDir())
+ putRawRelease(t, s, "bab", "km", "v1", []byte("bin"), "")
+ d, _, err := CanonicalDemand(ctx, unreadableStore{s}, "bab", "km", "v1", adoptPlatform, staged(t, opt))
+ if err != nil || d != DemandHold {
+ t.Fatalf("= %q, %v; want hold", d, err)
+ }
+ })
+ t.Run("no build for this platform", func(t *testing.T) {
+ opt := t.TempDir()
+ s := NewDirStore(t.TempDir())
+ putRawRelease(t, s, "bab", "km", "v1", []byte("bin"), "")
+ d, _, err := CanonicalDemand(ctx, s, "bab", "km", "v1", "plan9-mips", staged(t, opt))
+ if err != nil || d != DemandHold {
+ t.Fatalf("= %q, %v; want hold", d, err)
+ }
+ })
+ t.Run("staged file missing", func(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ putRawRelease(t, s, "bab", "km", "v1", []byte("bin"), "")
+ missing := VersionPath(t.TempDir(), "bab", "v1", "km")
+ if _, err := os.Stat(missing); err == nil {
+ t.Fatal("setup: the path exists")
+ }
+ d, sum, err := CanonicalDemand(ctx, s, "bab", "km", "v1", adoptPlatform, missing)
+ if err != nil || d != DemandHold || sum != "" {
+ t.Fatalf("= %q, %q, %v; want hold and no digest", d, sum, err)
+ }
+ })
+ t.Run("unknown demand", func(t *testing.T) {
+ opt := t.TempDir()
+ s := NewDirStore(t.TempDir())
+ putRawRelease(t, s, "bab", "km", "v1", []byte("bin"), `"swap":"drain",`)
+ d, _, err := CanonicalDemand(ctx, s, "bab", "km", "v1", adoptPlatform, staged(t, opt))
+ if err != nil || d != DemandHold {
+ t.Fatalf("= %q, %v; want hold", d, err)
+ }
+ })
+}