carlos / platform Public

host: the held-config identity is canonical JSON of the pinned etags, and an explicitly pinned empty configuration is granted (Task 10b fix round 2)

HeldConfigIdentity is "etags-v1:" plus encoding/json of {etags: the
non-empty bundle etags, overlay}: keys sorted, every delimiter escaped,
so {"default":"a;environment=b"} and {"default":"a","environment":"b"}
no longer share an identity. The empty configuration (an app without a
default bundle or an overlay pins exactly that at readiness) has its own
identity, the encoding of the empty set, and a held-config write naming
it is granted when Move.Inputs is present; missing Move.Inputs is still
refused.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 0431d3019f3b2cfc678f99a3ee381d81e8b42e17 parent 2c8f0dd
2 files changed, +105 −36
  • internal/host/movespolicy.go +34 −23
  • internal/host/movespolicy_test.go +71 −13
diff --git a/internal/host/movespolicy.go b/internal/host/movespolicy.go
index 0e3146d77..90781a060 100644
--- a/internal/host/movespolicy.go
+++ b/internal/host/movespolicy.go
@@ -2,10 +2,9 @@ package host
import (
"context"
+ "encoding/json"
"errors"
"fmt"
- "sort"
- "strings"
"github.com/carlosframework/platform/internal/hostbox"
"github.com/carlosframework/platform/internal/hostclaim"
@@ -50,9 +49,10 @@ import (
// landing), Move.Inputs must be pinned, a held-release identity must
// equal Inputs.Release, and a held-config identity must equal
// HeldConfigIdentity(Inputs.BundleETags, Inputs.OverlayETag), the
-// canonical form of the pinned etags. No pins, a record that is not this
-// landing, a read error or no reader refuse. The record is read for
-// these two actions only.
+// canonical form of the pinned etags (the empty configuration included:
+// Move.Inputs present is the pin). Missing Move.Inputs, a record that
+// is not this landing, a read error or no reader refuse. The record is
+// read for these two actions only.
//
// Production registration. RegisterProductionClaimOwners requires every
// owner, and the Inputs, PublicationEvidence and AdmissionEvidence owners
@@ -102,28 +102,39 @@ func MovesRecordsFrom(s release.Store) MovesRecordReader {
}
}
+// heldConfigIdentityPrefix names the encoding HeldConfigIdentity uses, so
+// a later form can be told apart from it.
+const heldConfigIdentityPrefix = "etags-v1:"
+
+// heldConfigPins is what HeldConfigIdentity encodes: the non-empty bundle
+// etags and the overlay etag. encoding/json writes map keys sorted and
+// escapes every delimiter, so two different sets never share an identity.
+type heldConfigPins struct {
+ ETags map[string]string `json:"etags"`
+ Overlay string `json:"overlay"`
+}
+
// HeldConfigIdentity is the identity a held-config write carries (§9.1):
// the bundle etags and the overlay etag it rendered from, in one canonical
-// string, so the policy can compare them with the move's pins exactly.
-// Keys are sorted; an empty etag is the same as an absent one. No etags at
-// all is the zero identity, which the policy never grants.
+// string, so the policy can compare them with the move's pins exactly. An
+// empty etag is the same as an absent one. The empty configuration (an app
+// with no default bundle and no overlay pins exactly that at readiness) has
+// its own identity, the encoding of the empty set; it is never the zero
+// identity.
func HeldConfigIdentity(bundleETags map[string]string, overlayETag string) hostwrite.Identity {
- var keys []string
+ pins := heldConfigPins{ETags: map[string]string{}, Overlay: overlayETag}
for k, v := range bundleETags {
if k != "" && v != "" {
- keys = append(keys, k)
+ pins.ETags[k] = v
}
}
- if len(keys) == 0 && overlayETag == "" {
+ raw, err := json.Marshal(pins)
+ if err != nil {
+ // Strings always marshal; a failure here would be a Go runtime
+ // fault, and the zero identity is refused everywhere.
return hostwrite.Identity{}
}
- sort.Strings(keys)
- parts := make([]string, 0, len(keys)+1)
- for _, k := range keys {
- parts = append(parts, k+"="+bundleETags[k])
- }
- parts = append(parts, "overlay="+overlayETag)
- return hostwrite.Identity{Config: "etags:" + strings.Join(parts, ";")}
+ return hostwrite.Identity{Config: heldConfigIdentityPrefix + string(raw)}
}
// The held actions and the landing's rights hostbox.Policy.Check asks
@@ -250,11 +261,11 @@ func MovesClaimPolicy(read MovesRecordReader) func(context.Context, *hostclaim.C
return refuse("the release is not the pinned one", nil)
}
case movesActionHeldConfig:
- pinned := HeldConfigIdentity(in.BundleETags, in.OverlayETag)
- if pinned.Config == "" {
- return refuse("no pinned config", nil)
- }
- if id.Config != pinned.Config {
+ // Move.Inputs present is the pin, empty etags included: an app
+ // without a default bundle or an overlay pins the empty
+ // configuration, and the render that names the same empty set
+ // is granted.
+ if id.Config != HeldConfigIdentity(in.BundleETags, in.OverlayETag).Config {
return refuse("the config is not the pinned one", nil)
}
}
diff --git a/internal/host/movespolicy_test.go b/internal/host/movespolicy_test.go
index 941219887..f4b28e030 100644
--- a/internal/host/movespolicy_test.go
+++ b/internal/host/movespolicy_test.go
@@ -368,12 +368,6 @@ func TestMovesClaimPolicyBindsHeldInputsToThePins(t *testing.T) {
for name, mutate := range map[string]func(*provision.Record){
"no inputs": func(r *provision.Record) { r.Move.Inputs = nil },
"no release pinned": func(r *provision.Record) { r.Move.Inputs.Release = "" },
- "no config pinned": func(r *provision.Record) {
- r.Move.Inputs.BundleETags, r.Move.Inputs.OverlayETag = nil, ""
- },
- "empty etags pinned": func(r *provision.Record) {
- r.Move.Inputs.BundleETags, r.Move.Inputs.OverlayETag = map[string]string{"default": "", "environment": ""}, ""
- },
} {
t.Run(name, func(t *testing.T) {
rec := mpRecord()
@@ -389,11 +383,6 @@ func TestMovesClaimPolicyBindsHeldInputsToThePins(t *testing.T) {
if errConfig != nil {
t.Fatalf("the config pins are intact: %v", errConfig)
}
- case "no config pinned", "empty etags pinned":
- refused(t, errConfig, "no pinned config")
- if errRelease != nil {
- t.Fatalf("the release pin is intact: %v", errRelease)
- }
default:
refused(t, errRelease, "no pinned inputs")
refused(t, errConfig, "no pinned inputs")
@@ -401,6 +390,70 @@ func TestMovesClaimPolicyBindsHeldInputsToThePins(t *testing.T) {
})
}
})
+ // An app without a default bundle or an overlay pins empty etags at
+ // readiness (movesheldenv.go): that is a configuration, with its own
+ // identity, granted when the render names the same empty set. Missing
+ // Move.Inputs is still refused.
+ t.Run("an explicitly pinned empty configuration is granted", func(t *testing.T) {
+ for name, pins := range map[string]*provision.MoveInputs{
+ "nil etags": {Release: "v1.2.3"},
+ "empty map": {BundleETags: map[string]string{}, Release: "v1.2.3"},
+ "all-empty etags": {BundleETags: map[string]string{"default": "", "environment": ""}, OverlayETag: "", Release: "v1.2.3"},
+ } {
+ t.Run(name, func(t *testing.T) {
+ rec := mpRecord()
+ rec.Move.Inputs = pins
+ moves := MovesClaimPolicy((&mpReader{rec: rec}).read)
+ c, s := at("held-config")
+ empty := HeldConfigIdentity(nil, "")
+ if empty.Config == "" {
+ t.Fatal("the empty configuration has no identity")
+ }
+ if err := moves(t.Context(), c, s, empty); err != nil {
+ t.Fatal(err)
+ }
+ if err := moves(t.Context(), c, s, HeldConfigIdentity(map[string]string{"default": "", "environment": ""}, "")); err != nil {
+ t.Fatalf("all-empty etags are the empty configuration: %v", err)
+ }
+ refused(t, moves(t.Context(), c, s, HeldConfigIdentity(map[string]string{"default": "e-default"}, "")), "the config is not the pinned one")
+ refused(t, moves(t.Context(), c, s, HeldConfigIdentity(nil, "e-overlay")), "the config is not the pinned one")
+ refused(t, moves(t.Context(), c, s, hostwrite.Identity{}), "the identity does not fit the action")
+ c, s = at("held-release")
+ if err := moves(t.Context(), c, s, hostwrite.Identity{Release: "v1.2.3"}); err != nil {
+ t.Fatal(err)
+ }
+ })
+ }
+ })
+ // The encoding is structural: a value that contains the delimiters of
+ // a flat encoding cannot collide with another set of etags.
+ t.Run("delimiters in an etag do not collide", func(t *testing.T) {
+ collisions := [][2]struct {
+ etags map[string]string
+ overlay string
+ }{
+ {{map[string]string{"default": "a;environment=b"}, ""}, {map[string]string{"default": "a", "environment": "b"}, ""}},
+ {{map[string]string{"default": "a"}, "x;environment=b"}, {map[string]string{"default": "a", "environment": "b"}, "x"}},
+ {{map[string]string{"default": "a;overlay=o"}, ""}, {map[string]string{"default": "a"}, "o"}},
+ {{map[string]string{"default=x": "a"}, ""}, {map[string]string{"default": "x=a"}, ""}},
+ {{map[string]string{"default": `a","environment":"b`}, ""}, {map[string]string{"default": "a", "environment": "b"}, ""}},
+ }
+ for i, pair := range collisions {
+ a := HeldConfigIdentity(pair[0].etags, pair[0].overlay)
+ b := HeldConfigIdentity(pair[1].etags, pair[1].overlay)
+ if a == b {
+ t.Fatalf("pair %d collides: %q", i, a.Config)
+ }
+ rec := mpRecord()
+ rec.Move.Inputs.BundleETags, rec.Move.Inputs.OverlayETag = pair[1].etags, pair[1].overlay
+ moves := MovesClaimPolicy((&mpReader{rec: rec}).read)
+ c, s := at("held-config")
+ refused(t, moves(t.Context(), c, s, a), "the config is not the pinned one")
+ if err := moves(t.Context(), c, s, b); err != nil {
+ t.Fatalf("pair %d: the pinned set refused: %v", i, err)
+ }
+ }
+ })
t.Run("a record read error", func(t *testing.T) {
cause := errors.New("the store is down")
moves := MovesClaimPolicy((&mpReader{err: cause}).read)
@@ -473,8 +526,13 @@ func TestMovesClaimPolicyBindsHeldInputsToThePins(t *testing.T) {
if a != b || a.Release != "" || a.Config == "" {
t.Fatalf("%+v vs %+v", a, b)
}
- if HeldConfigIdentity(nil, "").Config != "" {
- t.Fatal("no etags is an identity")
+ // The empty configuration has one identity, whichever way it is spelt.
+ e1, e2, e3 := HeldConfigIdentity(nil, ""), HeldConfigIdentity(map[string]string{}, ""), HeldConfigIdentity(map[string]string{"default": ""}, "")
+ if e1.Config == "" || e1 != e2 || e2 != e3 || e1 == b {
+ t.Fatalf("%+v %+v %+v", e1, e2, e3)
+ }
+ if HeldConfigIdentity(nil, "o") == e1 || HeldConfigIdentity(map[string]string{"default": "d"}, "") == e1 {
+ t.Fatal("an overlay or an etag alone is the empty configuration")
}
})
}