carlos / platform Public

docs: the region HA contracts note records the origin-qualified manifest, Publish and boxchoice signatures for the claim and moves sessions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 7cff1e76ea0a3cb34ff68732978d64731c030eca parent 8e3e867
1 file changed, +159 −0
  • docs/superpowers/notes/2026-10-07-region-ha-contracts.md +159 −0
diff --git a/docs/superpowers/notes/2026-10-07-region-ha-contracts.md b/docs/superpowers/notes/2026-10-07-region-ha-contracts.md
new file mode 100644
index 000000000..e06ceb98d
--- /dev/null
+++ b/docs/superpowers/notes/2026-10-07-region-ha-contracts.md
@@ -0,0 +1,159 @@
+# Region HA contracts for the claim and moves sessions
+
+Branch `region-ha-contracts`, 2026-10-07. Three contracts that the claim
+session (L13) and the moves session (L15, L17 in
+`2026-10-06-moves-region-ha-conflicts.md`) asked region HA for. All three
+are dormant: no production path calls them yet.
+
+## 1. Origin-qualified object references (claim L13)
+
+Package `internal/hostfiles`.
+
+The manifest format is now version 2 (`hostfiles.ManifestVersion = 2`).
+`Entry.Object` is the object's full key from the bucket root:
+
+ instances/<host>/<inc>/files/<gen>/objects/<16-digit seq>-<64 hex sha256>
+
+The publisher writes it under its own prefix. Readers fetch exactly that
+key and never join it to their own prefix. Version 1 held a key relative
+to the manifest's prefix. The field's meaning changed, so the version was
+bumped. Any manifest that is not version 2 (including 1) is refused by
+`Load`, `Open`, `Restore` and `CopyManifest` with "manifest <prefix> is
+version N; this binary reads 2". No version 1 manifest exists in
+production, so nothing needs migrating.
+
+Signatures:
+
+```go
+func Restore(ctx context.Context, s release.Store, prefix, dir string, origins []string) (Manifest, RestoreStats, error)
+func ReadPlatform(ctx context.Context, s release.Store, prefix string, m Manifest, name string, origins []string) ([]byte, error)
+func LoadWake(ctx context.Context, s release.Store, prefix string, m Manifest, origins []string) (WakeState, bool, error)
+func CopyManifest(ctx context.Context, s release.Store, fromPrefix, toPrefix string, origins []string) (Manifest, error)
+var ErrOrigin = errors.New("hostfiles: object origin not admitted")
+var ErrManifestExists = errors.New("hostfiles: the target generation already has a manifest")
+```
+
+`origins` is the set of generation prefixes that the caller's pin admits.
+nil or empty means the manifest's own prefix only. The set is used exactly
+as passed: a caller that passes a set must include the manifest's own
+prefix if it wants objects published there. Each origin must be a clean
+relative bucket key ending in `/`. If one is not, the call fails before
+anything is read.
+
+What Restore guarantees: it validates every path and every published
+entry before it touches `dir`. If any entry fails, Restore writes and
+removes nothing. An entry's object must:
+
+- be a clean bucket key: relative, no `..`, no `.`, no `//`, no NUL;
+- be exactly `<origin>objects/<%016d Seq>-<SHA256>`, using that entry's
+ own Seq and SHA256, where origin is one of the admitted origins. A key
+ nested deeper under an origin, a key under another host's or another
+ generation's prefix, a relative key in the version 1 style, an absolute
+ key, or a wrong seq or sha is refused. If the key is well formed but its
+ origin is not admitted, the error wraps `ErrOrigin`;
+- hash and size-check as before (`checkEntry`, then `fetch`).
+
+ReadPlatform and LoadWake apply the same rules to platform items.
+
+CopyManifest reads `fromPrefix`'s manifest and validates every entry
+against `origins` (nil means `fromPrefix` alone). A source built by an
+earlier copy references earlier generations, so its caller passes the
+whole chain. CopyManifest then writes the same bytes, with the same Seq and
+the same qualified references, to `toPrefix`'s manifest. The write is
+create-only.
+No objects are copied. Errors:
+
+- an absent source is `os.ErrNotExist`, never an empty copy;
+- a target already holding identical bytes is success, so a retry is safe;
+- any other target is `ErrManifestExists`, and the target is left as it
+ was;
+- a source equal to the target is refused.
+
+The new generation's `Open(toPrefix)` continues the sequence from the
+copy. An unchanged file keeps its old-generation reference. A changed file
+becomes a new object under `toPrefix`.
+
+For the claim's live set: the objects a manifest keeps alive are exactly
+the `Object` keys of its entries and platform items that are neither
+deleted nor skipped. Those keys may lie under earlier generations'
+prefixes.
+
+## 2. Files publish entry point (moves L17)
+
+Moves asked for `files.Publish(ctx, store, location, generation, dir,
+envPath) (manifestVersion string, err error)`. The closest stable call is:
+
+```go
+func Publish(ctx context.Context, s release.Store, prefix, dir string, skip func(rel string, isDir bool) bool) (int64, SyncStats, error)
+```
+
+- `prefix` is the generation's files prefix, `<location>files/<generation>/`.
+ The caller builds it.
+- It runs one `Open`, one `SyncDir` and one `Commit`, and returns the
+ committed manifest sequence number. That number is the manifest version.
+- On success, every regular file in `dir` that `skip` does not exclude is
+ in the manifest. A file larger than `MaxFileBytes` is recorded as
+ skipped and listed in `SyncStats.TooLarge`. Anything that is not a
+ regular file is left out and listed in `SyncStats.NotRegular`.
+- A file that exists but cannot be read is an error, and nothing is
+ committed.
+- A prefix that already has a manifest is continued, not replaced. A
+ repeat with nothing changed returns the same sequence number. If another
+ writer's manifest appears between Open and Commit, the error is
+ `ErrSuperseded`.
+- The env file is not part of this call. A caller that wants platform
+ items in the same revision uses `Open`, `StagePlatform`, `SyncDir` and
+ `Commit` directly.
+
+## 3. Eligible-boxes read (moves L15)
+
+New package `internal/boxchoice`. Console's unexported `eligibleBoxes` and
+`chooseBox` are now thin adapters over it, and their behaviour is
+unchanged. The predicate and choice tests moved to the new package.
+Console keeps tests that check its snapshot conversion carries every
+field.
+
+```go
+const Capability = "region-ha-1"
+type Headroom struct{ MemFreeMiB, DiskFreeMiB int64; Awake int }
+type Box struct {
+ Box, BoxID, EIP, Region, Placement string
+ HibernationConfigured, ServesSharedPool, AcceptsInbound *bool
+ SecretsKeyFP string
+ Capabilities []string
+ Draining bool
+ Headroom *Headroom
+ Fresh bool // not on the wire: the caller's liveness verdict
+}
+type Snapshot struct{ Boxes []Box; Complete bool }
+type RosterBox struct{ Placement string; Cloud boxid.Cloud }
+type Candidate struct{ BoxID, Region string; MemFreeMiB, DiskFreeMiB int64; Awake int }
+type Load struct{ Hosts int; Apps map[string]int }
+
+func Eligible(snap Snapshot, roster map[string]RosterBox, placement, awsRegion, homeName, expectedFP string, lost map[string]bool) []Candidate
+func Choose(host string, cands []Candidate, running map[string]Load, app string) (Candidate, bool)
+```
+
+`Box` uses the edges file's JSON tags, so an edges file unmarshals
+straight into it. The caller sets `Fresh` from the box's status snapshot.
+It sets `Snapshot.Complete` only when every listed edges file was read
+and every box's liveness is known.
+
+What Eligible guarantees:
+
+- It returns nothing when the snapshot is incomplete, `expectedFP` is
+ empty or `awsRegion` is empty.
+- It judges each box on that box's own evidence. Two partial boxes never
+ combine into one eligible box.
+- Identity and AWS region come from the roster. The edges file supplies
+ liveness and configuration.
+- The result is sorted by box id.
+
+What Choose guarantees: it prefers a box that does not run `app` (it never
+refuses on that), then the fewest hosts, then the greatest SHA-256 of
+`host + "\x00" + boxID`, then the smallest box id. The answer does not
+depend on the order of the candidates. It returns false only when there
+are no candidates.
+
+Moves' door can call `Eligible` then `Choose` in place of its "lexically
+first box" fallback.