carlos / platform Public

settings-1 final fixes: history-target refusals hold with the marker off

Rollback's resign refusal and the promote-onto-history digest binding run
whether or not the marker is present; only digest emission depends on it
(Astra r1 #3, P9 reversed). Adds the marker-unreadable refusal tests for
rollback and fast-track.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 8fe785493ae3a7261ce11a2e9cd7d44f4f7fb491 parent aaf5abb
2 files changed, +152 −3
  • internal/release/release.go +9 −3
  • internal/release/writers_test.go +143 −0
diff --git a/internal/release/release.go b/internal/release/release.go
index f6a29b26d..0c7594e76 100644
--- a/internal/release/release.go
+++ b/internal/release/release.go
@@ -807,12 +807,15 @@ func (sg Signer) claimRawPromote(ctx context.Context, s Store, acct, app, channe
if merr != nil {
return fmt.Errorf(TextMarkerUnreadable, "promote", acct, merr)
}
- if marker != nil && version != old.Version {
+ if version != old.Version {
// A promote onto a build the predecessor already lists is bound to
// that entry's signed digest (#253): without this it would sign
// whatever bytes the bucket holds under the name now, which is the
// replay the digests exist to stop. An approval-gated rollback runs
- // as exactly this promote.
+ // as exactly this promote. The binding holds with the marker off
+ // too (Astra r1 #3): suspending emission stops digests being
+ // written, not the refusal of a history entry the spec says is
+ // "never a rollback target once its channel has any digest".
for i, v := range old.History {
if v != version {
continue
@@ -1264,7 +1267,10 @@ func (sg Signer) claimRawRollbackAuthorized(ctx context.Context, s Store, acct,
return fmt.Errorf(TextMarkerUnreadable, "rollback", acct, merr)
}
hd := alignedHistoryDigests(old)
- if marker != nil && old.ChannelHasDigests() && hd[0] == "" {
+ // Marker or not (Astra r1 #3, spec §4.6): a history entry without a
+ // digest is never a rollback target once its channel has any digest.
+ // Only emission depends on the marker.
+ if old.ChannelHasDigests() && hd[0] == "" {
return &ResignNeededError{App: app, Channel: channel, Version: old.History[0]}
}
next := &ChannelPointer{
diff --git a/internal/release/writers_test.go b/internal/release/writers_test.go
index 2ba4bff63..ea88ae196 100644
--- a/internal/release/writers_test.go
+++ b/internal/release/writers_test.go
@@ -358,3 +358,146 @@ func TestPromoteOfAnUnchangedHistoryEntrySignsItsDigest(t *testing.T) {
t.Fatalf("promote back to v1: %+v; want the entry's signed digest %s", p, signed)
}
}
+
+// digestChannelThenMarkerOff is stable with v1 promoted before the marker
+// (no digest), v2 promoted under it (digests), and then the marker removed:
+// emission suspended on a channel that already carries digests.
+func digestChannelThenMarkerOff(t *testing.T) (Store, []byte, func() *ChannelPointer) {
+ t.Helper()
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
+ shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two"))
+ if err := Promote(ctx, s, priv, "bab", "km", "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ markerOn(t, s, priv, "bab")
+ if err := Promote(ctx, s, priv, "bab", "km", "stable", "v2"); err != nil {
+ t.Fatal(err)
+ }
+ if err := RemoveDigestMarker(ctx, s, "bab"); err != nil {
+ t.Fatal(err)
+ }
+ if m, err := ReadDigestMarker(ctx, s, pub, "bab"); m != nil || err != nil {
+ t.Fatalf("marker after removal = %+v, %v", m, err)
+ }
+ return s, priv, func() *ChannelPointer { return readPtr(t, s, pub, "stable") }
+}
+
+// Astra r1 #3, ruling P9 reversed: suspending emission does not make a
+// digest-less history entry a rollback target on a channel with digests.
+func TestRollbackOntoADigestlessEntryIsRefusedWithTheMarkerOff(t *testing.T) {
+ s, priv, cur := digestChannelThenMarkerOff(t)
+ before := cur()
+ err := Rollback(context.Background(), s, priv, "bab", "km", "stable")
+ var rn *ResignNeededError
+ if !errors.As(err, &rn) || rn.Version != "v1" {
+ t.Fatalf("marker off, rollback onto a digest-less v1 = %v; want ResignNeededError naming v1", err)
+ }
+ if after := cur(); after.Version != before.Version || !after.PromotedAt.Equal(before.PromotedAt) {
+ t.Fatalf("the refused rollback changed the pointer: %+v -> %+v", before, after)
+ }
+}
+
+// The approval-gated rollback runs as a hotfix promote of History[0]; with
+// the marker off it is refused the same way.
+func TestApprovalGatedRollbackOntoADigestlessEntryIsRefusedWithTheMarkerOff(t *testing.T) {
+ s, priv, cur := digestChannelThenMarkerOff(t)
+ before := cur()
+ _, err := PromoteOnLadder(context.Background(), s, priv, "bab", "km", "stable", "v1", PromoteOptions{Hotfix: true})
+ var rn *ResignNeededError
+ if !errors.As(err, &rn) || rn.Version != "v1" {
+ t.Fatalf("marker off, hotfix promote of a digest-less v1 = %v; want ResignNeededError naming v1", err)
+ }
+ if after := cur(); after.Version != before.Version || !after.PromotedAt.Equal(before.PromotedAt) {
+ t.Fatalf("the refused promote changed the pointer: %+v -> %+v", before, after)
+ }
+}
+
+// With the marker off, a promote back onto a history entry whose signed
+// digest the bucket's bytes no longer match is still refused.
+func TestPromoteOfAReshippedHistoryEntryIsRefusedWithTheMarkerOff(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ markerOn(t, s, priv, "bab")
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
+ shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two"))
+ for _, v := range []string{"v1", "v2"} {
+ if err := Promote(ctx, s, priv, "bab", "km", "stable", v); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if err := RemoveDigestMarker(ctx, s, "bab"); err != nil {
+ t.Fatal(err)
+ }
+ reship(t, s, "bab", "km", "v1", []byte("not what was promoted"))
+ if _, err := PromoteOnLadder(ctx, s, priv, "bab", "km", "stable", "v1", PromoteOptions{Hotfix: true}); !errors.Is(err, ErrManifestDigest) {
+ t.Fatalf("marker off, promote of a re-shipped history entry = %v; want ErrManifestDigest", err)
+ }
+ if p := readPtr(t, s, pub, "stable"); p.Version != "v2" {
+ t.Fatalf("the refused promote moved stable to %s", p.Version)
+ }
+}
+
+// Only emission depends on the marker: with it off, a rollback onto an
+// entry that carries a digest succeeds and writes no digest fields at all.
+func TestRollbackOntoADigestEntryWithTheMarkerOffWritesNoDigests(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ markerOn(t, s, priv, "bab")
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
+ shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two"))
+ for _, v := range []string{"v1", "v2"} {
+ if err := Promote(ctx, s, priv, "bab", "km", "stable", v); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if err := RemoveDigestMarker(ctx, s, "bab"); err != nil {
+ t.Fatal(err)
+ }
+ if err := Rollback(ctx, s, priv, "bab", "km", "stable"); err != nil {
+ t.Fatalf("marker off, rollback onto a digest-bearing v1: %v", err)
+ }
+ p := readPtr(t, s, pub, "stable")
+ if p.Version != "v1" || p.Digest != "" || p.HistoryDigests != nil {
+ t.Fatalf("after a marker-off rollback: %+v; want v1 with no digest fields", p)
+ }
+ raw, _, _ := s.Get(ctx, ChannelKey("bab", "km", "stable"))
+ if strings.Contains(string(raw), "digest") {
+ t.Fatal("the marker-off rollback's wrapper mentions digests")
+ }
+}
+
+// Task 5's deferred minor: an unverifiable marker refuses rollback and
+// fast-track as it refuses promote, and the pointer is unchanged.
+func TestRollbackAndFastTrackRefuseAnUnverifiableMarker(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
+ shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two"))
+ for _, v := range []string{"v1", "v2"} {
+ if err := Promote(ctx, s, priv, "bab", "km", "stable", v); err != nil {
+ t.Fatal(err)
+ }
+ }
+ other, _ := testKeys(t)
+ markerOn(t, s, other, "bab")
+ before := readPtr(t, s, pub, "stable")
+ _, merr := ReadDigestMarker(ctx, s, pub, "bab")
+ if merr == nil {
+ t.Fatal("control: a foreign-key marker verified")
+ }
+ if err := Rollback(ctx, s, priv, "bab", "km", "stable"); err == nil || err.Error() != fmt.Sprintf(TextMarkerUnreadable, "rollback", "bab", merr) {
+ t.Fatalf("Rollback with a foreign marker = %v; want the marker refusal", err)
+ }
+ if _, err := FastTrack(ctx, s, priv, "bab", "km", "stable", "ops@example.com"); err == nil || err.Error() != fmt.Sprintf(TextMarkerUnreadable, "fast-track", "bab", merr) {
+ t.Fatalf("FastTrack with a foreign marker = %v; want the marker refusal", err)
+ }
+ if after := readPtr(t, s, pub, "stable"); after.Version != before.Version || after.FastTrack || !after.PromotedAt.Equal(before.PromotedAt) {
+ t.Fatalf("a refused write changed the pointer: %+v -> %+v", before, after)
+ }
+}