carlos / platform Public

edges file: publish serves_shared_pool and accepts_inbound (nearest-region §6.1)

Computed from the reconcile config the box mints with, so an automatic
region choice can never land on a box that refuses ordinary instances.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 78c20eeeeb9a8c48fe41cc3556a757781ce1a14a parent f3be383
4 files changed, +176 −7
  • internal/host/edgesfile.go +29 −3
  • internal/host/edgesfile_test.go +70 −4
  • internal/provision/reconcile.go +12 −0
  • internal/provision/reconcile_eligibility_test.go +65 −0
diff --git a/internal/host/edgesfile.go b/internal/host/edgesfile.go
index 323fd1c89..c969126ac 100644
--- a/internal/host/edgesfile.go
+++ b/internal/host/edgesfile.go
@@ -203,9 +203,22 @@ type edgesFile struct {
// IAM role cannot reach the bucket it names, or whose
// CARLOS_HIBERNATE_REGION is wrong, publishes true here and still fails
// every wake. Naming it for what it actually knows is the point.
- HibernationConfigured bool `json:"hibernation_configured"`
- Generation string `json:"generation"`
- Hosts map[string]edgeHost `json:"hosts"`
+ HibernationConfigured bool `json:"hibernation_configured"`
+
+ // ServesSharedPool and AcceptsInbound are this box's own answers to
+ // "would my reconcile pass mint an ORDINARY instance record" — one in
+ // the shared "" pool, needing inbound service (nearest-region spec
+ // §6.1). Deliberately NOT omitempty, HibernationConfigured's precedent:
+ // absent is a writer that predates the field (unknown), false is a box
+ // that said no. The console needs both positively true before it makes
+ // an AUTOMATIC choice of this box's region; explicit requests ignore
+ // them. Computed from the same ReconcileConfig the reconcile pass uses
+ // (edgesFileEligibility), never a second reading of the environment.
+ ServesSharedPool bool `json:"serves_shared_pool"`
+ AcceptsInbound bool `json:"accepts_inbound"`
+
+ Generation string `json:"generation"`
+ Hosts map[string]edgeHost `json:"hosts"`
}
// edgesFilePass builds this box's edges file from the routes currently in
@@ -541,6 +554,14 @@ func edgesFileRegion(getenv func(string) string, probedRegion string) string {
return strings.TrimSpace(probedRegion)
}
+// edgesFileEligibility derives the two published eligibility answers from
+// the reconcile config this box actually runs with — provisionTickConfig,
+// the same parse the provision tick uses — so a spelling the reconciler
+// reads one way can never be published another.
+func edgesFileEligibility(getenv func(string) string) (sharedPool, inbound bool) {
+ return provisionTickConfig(getenv).SharedInstanceEligibility()
+}
+
// edgesFileLoop publishes this box's edges file every 5 minutes + jitter —
// the same cadence and arming site as registrySnapshotLoop — AND within one
// watch interval of any change to this box's registry (#174).
@@ -588,6 +609,9 @@ func edgesFileLoop(reg *registry.Registry) {
return
}
box := metricsBoxLabel(os.Getenv, readMachineID())
+ // Eligibility is read once here at arm, from the reconcile config's own
+ // parse, never a second reading of CARLOS_POOLS or CARLOS_INBOUND.
+ sharedPool, inbound := edgesFileEligibility(os.Getenv)
// Region resolved once here at arm — the domains loop's read-once
// precedent (domainsLoop reads its env once before the loop and threads
// the result through every tick via cfg, never re-reading or re-probing
@@ -608,6 +632,8 @@ func edgesFileLoop(reg *registry.Registry) {
// guarantee that is to derive both from the one read rather than to
// ask two questions that could drift.
HibernationConfigured: strings.TrimSpace(os.Getenv("CARLOS_HIBERNATE_BUCKET")) != "",
+ ServesSharedPool: sharedPool,
+ AcceptsInbound: inbound,
}
log.Printf("agent: edges file loop armed (5min + jitter, and %s after a registry change, %s)", edgesFileWatchInterval, edgesFileKey(box))
last := seedEdgesFileGate(ctx, store, box)
diff --git a/internal/host/edgesfile_test.go b/internal/host/edgesfile_test.go
index 75436e949..7b1d516fc 100644
--- a/internal/host/edgesfile_test.go
+++ b/internal/host/edgesfile_test.go
@@ -312,7 +312,7 @@ func TestEdgesFileRawWireBodyMatchesSchema(t *testing.T) {
t.Fatalf("generation %q is not RFC3339: %v", probe.Generation, err)
}
- want := `{"v":1,"box":"quiet-harbour","eip":"203.0.113.10","hibernation_configured":false,"generation":"` +
+ want := `{"v":1,"box":"quiet-harbour","eip":"203.0.113.10","hibernation_configured":false,"serves_shared_pool":false,"accepts_inbound":false,"generation":"` +
probe.Generation + `","hosts":{"app.example.com":{"role":"home","platform":false}}}`
if string(got) != want {
t.Fatalf("raw wire body =\n%s\nwant\n%s", got, want)
@@ -354,7 +354,7 @@ func TestEdgesFileRawWireBodyRegionField(t *testing.T) {
t.Fatal(err)
}
gen := genOf(t, got)
- want := `{"v":1,"box":"quiet-harbour","eip":"203.0.113.10","hibernation_configured":false,"generation":"` +
+ want := `{"v":1,"box":"quiet-harbour","eip":"203.0.113.10","hibernation_configured":false,"serves_shared_pool":false,"accepts_inbound":false,"generation":"` +
gen + `","hosts":{"app.example.com":{"role":"home","platform":false}}}`
if string(got) != want {
t.Fatalf("raw wire body (region unset) =\n%s\nwant\n%s", got, want)
@@ -373,7 +373,7 @@ func TestEdgesFileRawWireBodyRegionField(t *testing.T) {
t.Fatal(err)
}
gen := genOf(t, got)
- want := `{"v":1,"box":"quiet-harbour","eip":"203.0.113.10","region":"eu-west-1","hibernation_configured":false,"generation":"` +
+ want := `{"v":1,"box":"quiet-harbour","eip":"203.0.113.10","region":"eu-west-1","hibernation_configured":false,"serves_shared_pool":false,"accepts_inbound":false,"generation":"` +
gen + `","hosts":{"app.example.com":{"role":"home","platform":false}}}`
if string(got) != want {
t.Fatalf("raw wire body (region set) =\n%s\nwant\n%s", got, want)
@@ -926,7 +926,7 @@ func TestEdgesFileRawWireBodyPlacementField(t *testing.T) {
if err := json.Unmarshal(got, &probe); err != nil {
t.Fatal(err)
}
- want := `{"v":1,"box":"quiet-harbour","eip":"203.0.113.10","region":"eu-west-2","placement":"uk","hibernation_configured":false,"generation":"` +
+ want := `{"v":1,"box":"quiet-harbour","eip":"203.0.113.10","region":"eu-west-2","placement":"uk","hibernation_configured":false,"serves_shared_pool":false,"accepts_inbound":false,"generation":"` +
probe.Generation + `","hosts":{"app.example.com":{"role":"home","platform":false}}}`
if string(got) != want {
t.Fatalf("raw wire body (placement set) =\n%s\nwant\n%s", got, want)
@@ -1205,3 +1205,69 @@ func TestEdgesFilePublishesNoOwnerForAMixedPathPrefixHostname(t *testing.T) {
wantOwner(t, hosts, "one.example", "bae", "titogo")
wantOwner(t, hosts, "b.classic.tito.io", "bae", "classic")
}
+
+// The two eligibility fields are NOT omitempty: absent means "a writer
+// that predates the field" (unknown), false means "no" (spec §6.1).
+func TestEdgesFileRawWireBodyEligibilityFields(t *testing.T) {
+ for _, want := range []bool{true, false} {
+ s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
+ seedRoute(t, reg, "app.example.com")
+ var last []byte
+ ident := edgesFile{Box: "quiet-harbour", Region: "eu-west-2", Placement: "uk",
+ ServesSharedPool: want, AcceptsInbound: want}
+ if err := edgesFilePass(context.Background(), s, reg, ident, "203.0.113.10", &last); err != nil {
+ t.Fatal(err)
+ }
+ got, _, err := s.Get(context.Background(), "control/edges/quiet-harbour.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var raw map[string]any
+ if err := json.Unmarshal(got, &raw); err != nil {
+ t.Fatal(err)
+ }
+ for _, k := range []string{"serves_shared_pool", "accepts_inbound"} {
+ v, present := raw[k]
+ if !present || v != want {
+ t.Fatalf("%s = %v (present %v), want %v always published", k, v, present, want)
+ }
+ }
+ }
+}
+
+// What the loop publishes is the reconcile config's own answer, for every
+// spelling an operator might write. CARLOS_POOLS=",tito" parses to
+// ["tito"] (agent.go's parser drops empty elements), so it does NOT serve
+// the shared pool and must publish false (codex R3-3).
+func TestEdgesFileEligibilityFollowsTheReconcileConfig(t *testing.T) {
+ cases := []struct {
+ pools, inbound string
+ shared, accept bool
+ }{
+ {"", "", true, true},
+ {"tito", "", false, true},
+ {",tito", "", false, true},
+ {"", "none", true, false},
+ {"tito", "NONE", false, false},
+ }
+ for _, c := range cases {
+ getenv := func(k string) string {
+ switch k {
+ case "CARLOS_POOLS":
+ return c.pools
+ case "CARLOS_INBOUND":
+ return c.inbound
+ }
+ return ""
+ }
+ shared, inbound := edgesFileEligibility(getenv)
+ cfg := provisionTickConfig(getenv)
+ wantShared, wantInbound := cfg.SharedInstanceEligibility()
+ if shared != wantShared || inbound != wantInbound {
+ t.Fatalf("%+v: published (%v, %v) but the reconciler says (%v, %v)", c, shared, inbound, wantShared, wantInbound)
+ }
+ if shared != c.shared || inbound != c.accept {
+ t.Fatalf("%+v: got (%v, %v), want (%v, %v)", c, shared, inbound, c.shared, c.accept)
+ }
+ }
+}
diff --git a/internal/provision/reconcile.go b/internal/provision/reconcile.go
index 8b3d2d9e8..ffef7f3a9 100644
--- a/internal/provision/reconcile.go
+++ b/internal/provision/reconcile.go
@@ -1343,6 +1343,18 @@ func poolServed(pools []string, pool string) bool {
return slices.Contains(pools, pool)
}
+// SharedInstanceEligibility answers, from this box's own reconcile config,
+// whether it mints an ORDINARY instance record: one in the shared ""
+// pool, needing inbound service. The edges file publishes both answers
+// (serves_shared_pool, accepts_inbound) so the console never offers an
+// automatic choice of a region whose boxes would refuse the record
+// (nearest-region spec §6.1). It is the reconciler's own predicates —
+// poolServed and the CARLOS_INBOUND=none refusal in the reconcile pass — so
+// publication and minting cannot disagree.
+func (cfg ReconcileConfig) SharedInstanceEligibility() (sharedPool, inbound bool) {
+ return poolServed(cfg.Pools, ""), cfg.Inbound != InboundNone
+}
+
// seedJSON renders a manifest's seed argv as the wire text SetSeed and a
// minted route's Seed field carry: "" for nil/empty (no seed), else the
// JSON array of the argv strings.
diff --git a/internal/provision/reconcile_eligibility_test.go b/internal/provision/reconcile_eligibility_test.go
new file mode 100644
index 000000000..6f9ac5556
--- /dev/null
+++ b/internal/provision/reconcile_eligibility_test.go
@@ -0,0 +1,65 @@
+package provision
+
+import "testing"
+
+// SharedInstanceEligibility must be the reconciler's own predicates, never a
+// second reading of the environment (spec §6.1, codex R3-3).
+func TestSharedInstanceEligibilityIsTheMintPredicate(t *testing.T) {
+ cases := []struct {
+ pools []string
+ inbound string
+ shared, accept bool
+ }{
+ {nil, "", true, true},
+ {[]string{"tito"}, "", false, true},
+ {[]string{"", "tito"}, "", true, true},
+ {nil, InboundNone, true, false},
+ {[]string{"tito"}, InboundNone, false, false},
+ }
+ for _, c := range cases {
+ cfg := ReconcileConfig{Pools: c.pools, Inbound: c.inbound}
+ shared, inbound := cfg.SharedInstanceEligibility()
+ if shared != poolServed(c.pools, "") || inbound != (c.inbound != InboundNone) {
+ t.Fatalf("%+v: eligibility diverges from the mint predicates", c)
+ }
+ if shared != c.shared || inbound != c.accept {
+ t.Fatalf("%+v: got (%v, %v), want (%v, %v)", c, shared, inbound, c.shared, c.accept)
+ }
+ }
+}
+
+// spec §16, codex plan-R3 4: each published answer pairs with the reconcile
+// pass's own behaviour on an ORDINARY record (the shared "" pool, routed).
+// A box that publishes both true mints it; a box that publishes either
+// false refuses it, so the console's eligibility never offers a region
+// whose box would refuse the record.
+func TestEligibilityPairsWithTheReconcileRefusal(t *testing.T) {
+ for _, c := range []struct {
+ pools []string
+ inbound string
+ }{
+ {nil, ""}, {[]string{"tito"}, ""}, {[]string{"", "tito"}, ""},
+ {nil, InboundNone}, {[]string{"tito"}, InboundNone},
+ } {
+ s, sqid := setup(t)
+ if err := PutManifest(ctx, s, sqid, "tito", Manifest{Schema: 1, App: "tito", Domain: "go.tito.io"}); err != nil {
+ t.Fatal(err)
+ }
+ rec := acmeRecord(sqid)
+ rec.Pool = ""
+ if err := CreateRecord(ctx, s, rec); err != nil {
+ t.Fatal(err)
+ }
+ reg := regWith(t)
+ cfg := ReconcileConfig{Region: "ie", Pools: c.pools, DataDir: "/data", Inbound: c.inbound}
+ if err := Reconcile(ctx, s, reg, cfg); err != nil {
+ t.Fatal(err)
+ }
+ rt, err := reg.Get(rec.Host)
+ minted := err == nil && rt != nil
+ shared, inbound := cfg.SharedInstanceEligibility()
+ if minted != (shared && inbound) {
+ t.Errorf("pools %q inbound %q: minted %v, but the box publishes serves_shared_pool=%v accepts_inbound=%v", c.pools, c.inbound, minted, shared, inbound)
+ }
+ }
+}