moves: the agent asks root to stage T7's release and to publish the held row's (Task 7b)
Under the claim, with root's relay wired (armPhase1, the root control socket): - T7: a stage the agent cannot write (EROFS) is asked of root, stage only; T7 then passes only on the agent's own stage of the same version, which finds it staged and verifies it. Root's answer is a hint. No root route (an older build) is MV-N455, and nothing is left for the old file relay. - configured: the agent never publishes. It sends the claim tuple it observes, the move id, the row's hold op (the rollback id for a return) and the render hash configured journalled, and passes on its own VerifyHeldLive reading. A release already live asks nothing. - heldReleaseProbeRelease: the probe owners' Release, root's publication after the agent's own selection is checked once. Not yet registered in production (the probe owners are unarmed). With the relay nil it is Task 7's behaviour; with the claim off nothing changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 files changed,
+419
−10
internal/host/movesheldrelease_agent_test.go+321 −0internal/host/movespass.go+12 −1internal/host/movesprobeowners.go+5 −2internal/host/movesstage.go+81 −7
diff --git a/internal/host/movesheldrelease_agent_test.go b/internal/host/movesheldrelease_agent_test.go| new file mode 100644 |
| index 000000000..f3bdded4b |
| --- /dev/null |
| +++ b/internal/host/movesheldrelease_agent_test.go |
| @@ -0,0 +1,321 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "crypto/ed25519" |
| + "errors" |
| + "fmt" |
| + "os" |
| + "path/filepath" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/hostwrite" |
| + "github.com/carlosframework/platform/internal/provision" |
| + "github.com/carlosframework/platform/internal/registry" |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +// Task 7b, the agent's side: under the claim with root's relay wired, T7's |
| +// blocked stage and every publication are asked of root (movesstage.go, |
| +// heldReleaseHooksVia; the probe's heldReleaseProbeRelease), and the agent |
| +// passes only on its own reading afterwards. Root's side and the fixture |
| +// are in movesheldrelease_test.go. |
| + |
| +// heldReleaseRelayFunc is a relay that calls root directly, as the socket |
| +// transport would after authenticating the agent. |
| +type heldReleaseRelayFunc func(context.Context, heldReleaseRequest) (heldReleaseResponse, error) |
| + |
| +func (f heldReleaseRelayFunc) HeldRelease(ctx context.Context, q heldReleaseRequest) (heldReleaseResponse, error) { |
| + return f(ctx, q) |
| +} |
| + |
| +func (f *hrFixture) relay(seen *[]heldReleaseRequest) heldReleaseRelay { |
| + return heldReleaseRelayFunc(func(ctx context.Context, q heldReleaseRequest) (heldReleaseResponse, error) { |
| + if seen != nil { |
| + *seen = append(*seen, q) |
| + } |
| + return f.root.Serve(ctx, hrPeer, q) |
| + }) |
| +} |
| + |
| +// phase1Root is root on the target box of a Phase 1 harness, over the |
| +// harness's own claim, record and source report. |
| +func phase1Root(t *testing.T, h *phase1Harness, opt, data string) *movesHeldReleaseRoot { |
| + t.Helper() |
| + return &movesHeldReleaseRoot{ |
| + Actor: func(context.Context) (hostbox.Actor, error) { |
| + return hostbox.Actor{Box: "uk-box", Region: "uk", Eligible: true}, nil |
| + }, |
| + Store: h.s, |
| + Claims: func(ctx context.Context, host string) (hostclaim.Observed, error) { |
| + return hostclaim.Read(ctx, h.s, host) |
| + }, |
| + Records: MovesRecordsFrom(h.s), |
| + Route: func(string) (*registry.Route, error) { return nil, nil }, |
| + Works: func(context.Context, hostbox.Actor, string, string) (hostwrite.Work, error) { |
| + t.Fatal("a stage selected") |
| + return nil, nil |
| + }, |
| + Selected: hostbox.Selected, |
| + Pub: hsPub, |
| + Ratchet: func() *release.DigestRatchet { return hsRatchet(t) }, |
| + Proof: func() *release.ProofGate { return offProofGate(t) }, |
| + OptDir: opt, |
| + DataDir: data, |
| + ReceiptDir: t.TempDir(), |
| + } |
| +} |
| + |
| +// blockedOnce makes the agent's version store read-only, as on a hardened |
| +// box: a stage that would write fails as EROFS does, and a version already |
| +// staged (by root) is read and verified as release.StageHeldVerified reads |
| +// it. |
| +func blockedOnce(t *testing.T) { |
| + t.Helper() |
| + prev := heldStageVerified |
| + t.Cleanup(func() { heldStageVerified = prev }) |
| + heldStageVerified = func(ctx context.Context, s release.Store, pub ed25519.PublicKey, r *release.DigestRatchet, optDir string, rt registry.Route, version string, proof *release.ProofGate) (*release.Manifest, error) { |
| + if _, err := os.Stat(release.VersionPath(optDir, release.RouteAccount(rt), version, rt.App)); err != nil { |
| + return nil, fmt.Errorf("%w: read-only file system", release.ErrStageBlocked) |
| + } |
| + return prev(ctx, s, pub, r, optDir, rt, version, proof) |
| + } |
| +} |
| + |
| +// TestHeldReleaseStagesThroughRootWithoutPublishing: on a hardened box the |
| +// agent's stage is EROFS; under the claim T7 asks root to stage, root |
| +// stages and verifies without publishing, and T7 passes on the agent's own |
| +// re-verification of the staged bytes. Nothing is left for the old file |
| +// relay, and nothing is selected. |
| +func TestHeldReleaseStagesThroughRootWithoutPublishing(t *testing.T) { |
| + ctx := context.Background() |
| + h := newPhase1Harness(t) |
| + rec := h.start(t, "manual") |
| + shipPinnedV7(t, h.s) |
| + h.pass(t) |
| + t.Setenv("CARLOS_HOST_CLAIM", "1") |
| + opt, data := t.TempDir(), t.TempDir() |
| + root := phase1Root(t, h, opt, data) |
| + blockedOnce(t) |
| + var seen []heldReleaseRequest |
| + relay := heldReleaseRelayFunc(func(ctx context.Context, q heldReleaseRequest) (heldReleaseResponse, error) { |
| + seen = append(seen, q) |
| + return root.Serve(ctx, hrPeer, q) |
| + }) |
| + sel := &heldSelections{} |
| + relayedFile := false |
| + ready, _ := heldReleaseHooksVia(heldReleaseVia{Relay: relay, Region: "uk"}, claimWrites(sel), testRegistry(t), opt, data, "uk-box", |
| + hsPub, hsRatchet(t), offProofGate(t), func(string, string, error) { relayedFile = true }) |
| + if c := ready(ctx, h.s, rec, "v7"); !c.OK { |
| + t.Fatalf("T7 through root = %+v", c) |
| + } |
| + if len(seen) != 1 || seen[0].Action != heldReleaseStage || seen[0].Version != "v7" || seen[0].Kind != hostclaim.KindMoving || seen[0].RenderHash != "" { |
| + t.Fatalf("root was asked %+v", seen) |
| + } |
| + if _, err := os.Stat(release.VersionPath(opt, "bab", "v7", "app")); err != nil { |
| + t.Fatalf("v7 not staged: %v", err) |
| + } |
| + if _, err := os.Lstat(release.LiveLink(opt, p1Host)); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatalf("root's stage published live/<host>: %v", err) |
| + } |
| + if _, err := os.Stat(release.HeldStageDir(data)); !errors.Is(err, os.ErrNotExist) || relayedFile { |
| + t.Fatalf("a file request was left for the old relay (%v, %v)", err, relayedFile) |
| + } |
| + if len(sel.selected) != 0 { |
| + t.Fatalf("T7 selected %v", sel.selected) |
| + } |
| +} |
| + |
| +// TestHeldReleaseConfiguredPublishesThroughRoot: under the claim with root's |
| +// relay wired, configured's release hook never publishes in the agent: it |
| +// asks root with the claim's tuple and the journalled render hash, and then |
| +// passes on its own reading. A replay finds the release live and asks |
| +// nothing. |
| +func TestHeldReleaseConfiguredPublishesThroughRoot(t *testing.T) { |
| + ctx := context.Background() |
| + t.Setenv("CARLOS_HOST_CLAIM", "1") |
| + f := newHRFixture(t) |
| + var seen []heldReleaseRequest |
| + agentSel := &heldSelections{} |
| + _, live := heldReleaseHooksVia(heldReleaseVia{Relay: f.relay(&seen), Region: "uk", Claims: f.claims}, claimWrites(agentSel), testRegistry(t), f.opt, f.data, "uk-box", |
| + hsPub, hsRatchet(t), offProofGate(t), nil) |
| + if c := live(ctx, f.s, f.rec, "v7"); !c.OK { |
| + t.Fatalf("configured's release through root = %+v", c) |
| + } |
| + if len(seen) != 1 { |
| + t.Fatalf("root was asked %d times", len(seen)) |
| + } |
| + q := seen[0] |
| + if q.Action != heldReleasePublish || q.RenderHash != hrRendered || q.HoldOp != p1ID || q.MoveID != p1ID || q.Stamp != 7 || q.Session != 3 || |
| + q.Generation != p1ID || q.Kind != hostclaim.KindLanding || q.Phase != provision.LandConfigured || q.Region != "uk" || q.Box != "uk-box" { |
| + t.Fatalf("the agent's request = %+v", q) |
| + } |
| + if len(agentSel.selected) != 0 { |
| + t.Fatalf("the agent selected %v itself", agentSel.selected) |
| + } |
| + if v, _ := release.LiveVersion(f.opt, p1Host); v != "v7" { |
| + t.Fatalf("live = %q", v) |
| + } |
| + if c := live(ctx, f.s, f.rec, "v7"); !c.OK || len(seen) != 1 { |
| + t.Fatalf("a replay: %+v, asked %d times", c, len(seen)) |
| + } |
| + |
| + t.Run("not yet waits for the next pass", func(t *testing.T) { |
| + g := newHRFixture(t) |
| + g.root.DataDir = t.TempDir() |
| + _, live := heldReleaseHooksVia(heldReleaseVia{Relay: g.relay(nil), Region: "uk", Claims: g.claims}, claimWrites(&heldSelections{}), testRegistry(t), g.opt, g.data, "uk-box", |
| + hsPub, hsRatchet(t), offProofGate(t), nil) |
| + if c := live(ctx, g.s, g.rec, "v7"); c.OK { |
| + t.Fatalf("published with root's journal missing: %+v", c) |
| + } |
| + if v, _ := release.LiveVersion(g.opt, p1Host); v != "" { |
| + t.Fatalf("live = %q", v) |
| + } |
| + }) |
| +} |
| + |
| +// TestHeldReleaseProbeReleaseAdapter: the probe's release adapter publishes |
| +// through root when live/<host> does not name the pinned release, and asks |
| +// nothing when it does. |
| +func TestHeldReleaseProbeReleaseAdapter(t *testing.T) { |
| + ctx := context.Background() |
| + f := newHRFixture(t) |
| + f.set(func(f *hrFixture) { |
| + f.claim.Op.Phase = provision.LandProbing |
| + f.rec.Move.LandingPhase = provision.LandProbing |
| + }) |
| + var seen []heldReleaseRequest |
| + adapter := heldReleaseProbeRelease(heldReleaseVia{Relay: f.relay(&seen), Region: "uk", Claims: f.claims}, f.s, "uk-box", f.opt, f.data, offProofGate(t)) |
| + rt := registry.Route{Host: p1Host, Kind: "instance", App: "app", Account: "bab", HoldOp: p1ID} |
| + if err := adapter(ctx, rt, "v7", heldSelectionWork{&heldSelections{}}); err != nil { |
| + t.Fatalf("the probe's release: %v", err) |
| + } |
| + if len(seen) != 1 || seen[0].Phase != provision.LandProbing || seen[0].Action != heldReleasePublish { |
| + t.Fatalf("root was asked %+v", seen) |
| + } |
| + if err := adapter(ctx, rt, "v7", heldSelectionWork{&heldSelections{}}); err != nil || len(seen) != 1 { |
| + t.Fatalf("a live release: %v, asked %d times", err, len(seen)) |
| + } |
| +} |
| + |
| +// TestHeldReleaseOldConsumers: a root that has no held-release route (an |
| +// older build, or the claim off) answers unavailable, and T7 says MV-N455 |
| +// with nothing staged, published or left as a file; and the old file relay, |
| +// run after the new path staged, finds no request and publishes nothing. |
| +func TestHeldReleaseOldConsumers(t *testing.T) { |
| + ctx := context.Background() |
| + |
| + t.Run("an old root", func(t *testing.T) { |
| + h := newPhase1Harness(t) |
| + rec := h.start(t, "manual") |
| + shipPinnedV7(t, h.s) |
| + h.pass(t) |
| + t.Setenv("CARLOS_HOST_CLAIM", "1") |
| + opt, data := t.TempDir(), t.TempDir() |
| + blockedOnce(t) |
| + old := heldReleaseRelayFunc(func(context.Context, heldReleaseRequest) (heldReleaseResponse, error) { |
| + return heldReleaseResponse{}, errHeldReleaseUnavailable |
| + }) |
| + ready, _ := heldReleaseHooksVia(heldReleaseVia{Relay: old, Region: "uk"}, claimWrites(&heldSelections{}), testRegistry(t), opt, data, "uk-box", |
| + hsPub, hsRatchet(t), offProofGate(t), nil) |
| + if c := ready(ctx, h.s, rec, "v7"); c.OK || c.Reason != t7NotStaged { |
| + t.Fatalf("T7 against an old root = %+v, want %q", c, t7NotStaged) |
| + } |
| + if _, err := os.Stat(filepath.Join(opt, "versions")); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatalf("staged: %v", err) |
| + } |
| + if _, err := os.Stat(release.HeldStageDir(data)); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatalf("a file request was left for the old relay: %v", err) |
| + } |
| + }) |
| + |
| + t.Run("the old file relay publishes nothing", func(t *testing.T) { |
| + h := newPhase1Harness(t) |
| + rec := h.start(t, "manual") |
| + shipPinnedV7(t, h.s) |
| + h.pass(t) |
| + t.Setenv("CARLOS_HOST_CLAIM", "1") |
| + opt, data := t.TempDir(), t.TempDir() |
| + root := phase1Root(t, h, opt, data) |
| + blockedOnce(t) |
| + ready, _ := heldReleaseHooksVia(heldReleaseVia{Relay: heldReleaseRelayFunc(func(ctx context.Context, q heldReleaseRequest) (heldReleaseResponse, error) { |
| + return root.Serve(ctx, hrPeer, q) |
| + }), Region: "uk"}, claimWrites(&heldSelections{}), testRegistry(t), opt, data, "uk-box", hsPub, hsRatchet(t), offProofGate(t), nil) |
| + if c := ready(ctx, h.s, rec, "v7"); !c.OK { |
| + t.Fatalf("T7 = %+v", c) |
| + } |
| + t.Setenv("CARLOS_HOST_CLAIM", "") |
| + authorized := false |
| + if err := release.ServeHeldStageRequests(ctx, h.s, hsPub, hsRatchet(t), opt, data, offProofGate(t), func(release.HeldStageRequest, func() error) error { |
| + authorized = true |
| + return nil |
| + }); err != nil { |
| + t.Fatal(err) |
| + } |
| + if authorized { |
| + t.Fatal("the old relay found a request") |
| + } |
| + if _, err := os.Lstat(release.LiveLink(opt, p1Host)); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatalf("the old relay published: %v", err) |
| + } |
| + }) |
| +} |
| + |
| +// TestHeldReleaseAgentVerifiesRootsStage: root's answer to a stage is a |
| +// hint. T7 passes only on the agent's own reading of the staged bytes, so a |
| +// root that answers without staging never passes T7, and a stage whose |
| +// answer was lost passes on the next pass's reading without asking root |
| +// again. |
| +func TestHeldReleaseAgentVerifiesRootsStage(t *testing.T) { |
| + ctx := context.Background() |
| + |
| + t.Run("root's answer is a hint", func(t *testing.T) { |
| + h := newPhase1Harness(t) |
| + rec := h.start(t, "manual") |
| + shipPinnedV7(t, h.s) |
| + h.pass(t) |
| + t.Setenv("CARLOS_HOST_CLAIM", "1") |
| + opt, data := t.TempDir(), t.TempDir() |
| + blockedOnce(t) |
| + lying := heldReleaseRelayFunc(func(_ context.Context, q heldReleaseRequest) (heldReleaseResponse, error) { |
| + return heldReleaseResponse{Schema: heldReleaseSchema, RequestID: q.RequestID, Outcome: heldReleaseOutcomeStaged}, nil |
| + }) |
| + ready, _ := heldReleaseHooksVia(heldReleaseVia{Relay: lying, Region: "uk"}, claimWrites(&heldSelections{}), testRegistry(t), opt, data, "uk-box", |
| + hsPub, hsRatchet(t), offProofGate(t), nil) |
| + if c := ready(ctx, h.s, rec, "v7"); c.OK || c.Reason != t7NotStaged { |
| + t.Fatalf("T7 on root's word alone = %+v, want %q", c, t7NotStaged) |
| + } |
| + }) |
| + |
| + t.Run("a stage whose answer is lost", func(t *testing.T) { |
| + h := newPhase1Harness(t) |
| + rec := h.start(t, "manual") |
| + shipPinnedV7(t, h.s) |
| + h.pass(t) |
| + t.Setenv("CARLOS_HOST_CLAIM", "1") |
| + opt, data := t.TempDir(), t.TempDir() |
| + root := phase1Root(t, h, opt, data) |
| + blockedOnce(t) |
| + asked := 0 |
| + lost := heldReleaseRelayFunc(func(ctx context.Context, q heldReleaseRequest) (heldReleaseResponse, error) { |
| + asked++ |
| + if _, err := root.Serve(ctx, hrPeer, q); err != nil { |
| + return heldReleaseResponse{}, err |
| + } |
| + return heldReleaseResponse{}, errors.New("the connection dropped before the answer") |
| + }) |
| + ready, _ := heldReleaseHooksVia(heldReleaseVia{Relay: lost, Region: "uk"}, claimWrites(&heldSelections{}), testRegistry(t), opt, data, "uk-box", |
| + hsPub, hsRatchet(t), offProofGate(t), nil) |
| + if c := ready(ctx, h.s, rec, "v7"); c.OK { |
| + t.Fatalf("T7 passed on a lost answer: %+v", c) |
| + } |
| + if c := ready(ctx, h.s, rec, "v7"); !c.OK { |
| + t.Fatalf("T7 after the lost answer: %+v", c) |
| + } |
| + if asked != 1 { |
| + t.Fatalf("root asked %d times, want once: the staged bytes are verified by the agent itself", asked) |
| + } |
| + }) |
| +} |
diff --git a/internal/host/movespass.go b/internal/host/movespass.go| index 149c0bf26..8aabbb275 100644 |
| --- a/internal/host/movespass.go |
| +++ b/internal/host/movespass.go |
| @@ -17,6 +17,7 @@ import ( |
| "github.com/carlosframework/platform/internal/activator" |
| "github.com/carlosframework/platform/internal/appconfig" |
| + "github.com/carlosframework/platform/internal/controlsock" |
| "github.com/carlosframework/platform/internal/dbverify" |
| "github.com/carlosframework/platform/internal/hostbox" |
| "github.com/carlosframework/platform/internal/hostclaim" |
| @@ -540,7 +541,17 @@ func armPhase1(cfg *moveCfg, w *movesWorker, hib activator.Config, dataDir strin |
| // verifies without publishing, and the configured step publishes |
| // under the held-release selection (heldReleaseHooks). With the |
| // claim off both are heldReleaseReady, as before. |
| - cfg.ReleaseReady, cfg.ReleaseLive = heldReleaseHooks(writes, reg, cfg.OptDir, dataDir, cfg.Box, pub, pointerRatchet(false, cfg.OptDir, dataDir, getenv), |
| + // |
| + // Task 7b: under the claim the agent cannot write /opt/carlos, so a |
| + // blocked stage and every publication are asked of root over its |
| + // control socket (heldReleaseHooksVia), which re-reads the claim, |
| + // the record and the render journal and publishes only inside its |
| + // own held-release selection. |
| + via := heldReleaseVia{Region: cfg.Region} |
| + if writes.Enabled { |
| + via.Relay = newHeldReleaseRootClient(controlsock.Path) |
| + } |
| + cfg.ReleaseReady, cfg.ReleaseLive = heldReleaseHooksVia(via, writes, reg, cfg.OptDir, dataDir, cfg.Box, pub, pointerRatchet(false, cfg.OptDir, dataDir, getenv), |
| release.NewProofGate(release.ProofPolicyPath, dataDir), stageMarkerRequester(dataDir)) |
| if svc != nil { |
| // S1 asks the instance itself, woken through the activator. |
diff --git a/internal/host/movesprobeowners.go b/internal/host/movesprobeowners.go| index 7fda90e53..731f68ec6 100644 |
| --- a/internal/host/movesprobeowners.go |
| +++ b/internal/host/movesprobeowners.go |
| @@ -129,8 +129,11 @@ type movesProbeOwners struct { |
| Admitted func(context.Context, *provision.Record) (int, error) |
| // Works selects a held action's claim work at the admitted capture |
| // (hostbox.WorkSource.SelectAt). |
| - Works func(ctx context.Context, host, action string, captured hostguard.Captured) (hostwrite.Work, error) |
| - Render func(ctx context.Context, rt registry.Route, op string, work func(appconfig.HeldRender) (hostwrite.Work, error)) (appconfig.HeldRender, error) |
| + Works func(ctx context.Context, host, action string, captured hostguard.Captured) (hostwrite.Work, error) |
| + Render func(ctx context.Context, rt registry.Route, op string, work func(appconfig.HeldRender) (hostwrite.Work, error)) (appconfig.HeldRender, error) |
| + // Release makes version live for the held row before a start. On a box |
| + // whose agent cannot write /opt/carlos it is heldReleaseProbeRelease |
| + // (Task 7b): root publishes, inside root's own held-release selection. |
| Release func(ctx context.Context, rt registry.Route, version string, work hostwrite.Work) error |
| NewID func() string |
| Now func() time.Time |
diff --git a/internal/host/movesstage.go b/internal/host/movesstage.go| index d2d9b17f7..8ac9497f7 100644 |
| --- a/internal/host/movesstage.go |
| +++ b/internal/host/movesstage.go |
| @@ -42,11 +42,21 @@ var heldStageVerified = release.StageHeldVerified |
| // is heldReleasePublished, which publishes under the landing's held-release |
| // selection, where MovesClaimPolicy grants it, and then reads it back. |
| func heldReleaseHooks(writes hostwrite.Config, reg *registry.Registry, optDir, dataDir, box string, pub ed25519.PublicKey, r *release.DigestRatchet, proof *release.ProofGate, requestStage func(host, ver string, err error)) (ready, live func(context.Context, release.Store, provision.Record, string) moves.Check) { |
| + return heldReleaseHooksVia(heldReleaseVia{}, writes, reg, optDir, dataDir, box, pub, r, proof, requestStage) |
| +} |
| + |
| +// heldReleaseHooksVia is heldReleaseHooks with root's held-release path |
| +// (Task 7b): under the claim, with via.Relay set, a stage this process |
| +// cannot write is asked of root (stage only, then verified here), and the |
| +// publication is root's, inside root's own held-release selection; the |
| +// agent never publishes live/<host> itself. With via.Relay nil it is Task |
| +// 7's behaviour. With the claim off via is not consulted. |
| +func heldReleaseHooksVia(via heldReleaseVia, writes hostwrite.Config, reg *registry.Registry, optDir, dataDir, box string, pub ed25519.PublicKey, r *release.DigestRatchet, proof *release.ProofGate, requestStage func(host, ver string, err error)) (ready, live func(context.Context, release.Store, provision.Record, string) moves.Check) { |
| if !writes.Enabled { |
| ready = heldReleaseReady(reg, optDir, dataDir, box, pub, r, proof, requestStage) |
| return ready, ready |
| } |
| - return heldReleaseStaged(reg, optDir, dataDir, box, pub, r, proof), heldReleasePublished(writes, optDir, dataDir, pub, r, proof) |
| + return heldReleaseStaged(via, reg, optDir, dataDir, box, pub, r, proof), heldReleasePublished(via, writes, optDir, dataDir, box, pub, r, proof) |
| } |
| // heldReleaseStaged is T7 under the claim (Paul, 2026-10-12): the pinned |
| @@ -58,10 +68,16 @@ func heldReleaseHooks(writes hostwrite.Config, reg *registry.Registry, optDir, d |
| // this release). live/<host> is never touched and nothing is selected: the |
| // publication is the configured step's (heldReleasePublished). |
| // |
| -// A version store this process cannot write is t7NotStaged. Nothing is |
| -// relayed to the root stage pass: its relay publishes, and it refuses held |
| -// rows while CARLOS_HOST_CLAIM is on, so a request would wait forever. |
| -func heldReleaseStaged(reg *registry.Registry, optDir, dataDir, box string, pub ed25519.PublicKey, r *release.DigestRatchet, proof *release.ProofGate) func(context.Context, release.Store, provision.Record, string) moves.Check { |
| +// A version store this process cannot write is asked of root over its |
| +// control socket (Task 7b, via.Relay): root re-reads the claim, the record |
| +// and the source's report, stages and verifies, and never publishes. Root's |
| +// answer is a hint: T7 passes only on this process's own stage of the same |
| +// version afterwards, which finds the bytes staged and verifies them |
| +// (writing nothing to the version store when they are the release's). With |
| +// no relay, or a root that has no such route, a blocked stage is |
| +// t7NotStaged. Nothing is ever left for the old file relay: it publishes, |
| +// and it refuses held rows while CARLOS_HOST_CLAIM is on. |
| +func heldReleaseStaged(via heldReleaseVia, reg *registry.Registry, optDir, dataDir, box string, pub ed25519.PublicKey, r *release.DigestRatchet, proof *release.ProofGate) func(context.Context, release.Store, provision.Record, string) moves.Check { |
| return func(ctx context.Context, s release.Store, rec provision.Record, version string) moves.Check { |
| req := release.HeldStageRequest{Account: rec.Account, App: rec.App, Host: rec.Host, Version: version, Op: rec.Move.ID, Box: box} |
| if err := heldStageAuthority(reg, s, box, dataDir)(req, nil); err != nil { |
| @@ -70,13 +86,19 @@ func heldReleaseStaged(reg *registry.Registry, optDir, dataDir, box string, pub |
| } |
| rt := registry.Route{Host: rec.Host, Kind: "instance", App: rec.App, Account: rec.Account, HoldOp: rec.Move.ID} |
| _, err := heldStageVerified(ctx, s, pub, r, optDir, rt, version, proof) |
| + if errors.Is(err, release.ErrStageBlocked) && via.Relay != nil { |
| + logMoveStage(rec.Host, version, err) |
| + if err = via.stage(ctx, s, rec, box, version); err == nil { |
| + _, err = heldStageVerified(ctx, s, pub, r, optDir, rt, version, proof) |
| + } |
| + } |
| switch { |
| case err == nil: |
| return moves.Check{OK: true} |
| case errors.Is(err, release.ErrHeldNotProven): |
| logMoveStage(rec.Host, version, err) |
| return moves.Check{Reason: t7NotProven} |
| - case errors.Is(err, release.ErrStageBlocked): |
| + case errors.Is(err, release.ErrStageBlocked), errors.Is(err, errHeldReleaseUnavailable): |
| logMoveStage(rec.Host, version, err) |
| return moves.Check{Reason: t7NotStaged} |
| default: |
| @@ -97,7 +119,14 @@ func heldReleaseStaged(reg *registry.Registry, optDir, dataDir, box string, pub |
| // then passes only on release.VerifyHeldLive under that lock, the reading T7 |
| // makes with the claim off. A link already live and proven is not published |
| // again, so a replay selects nothing. |
| -func heldReleasePublished(writes hostwrite.Config, optDir, dataDir string, pub ed25519.PublicKey, r *release.DigestRatchet, proof *release.ProofGate) func(context.Context, release.Store, provision.Record, string) moves.Check { |
| +// |
| +// With via.Relay set (Task 7b, production under the claim) the agent never |
| +// publishes: it asks root, with the claim tuple it observes and the render |
| +// hash configured journalled under the row's hold op, and root publishes |
| +// inside its own held-release selection. The agent then passes only on its |
| +// own reading. Root's not-yet, refusal or absence fails this pass, and the |
| +// configured step asks again on the next. |
| +func heldReleasePublished(via heldReleaseVia, writes hostwrite.Config, optDir, dataDir, box string, pub ed25519.PublicKey, r *release.DigestRatchet, proof *release.ProofGate) func(context.Context, release.Store, provision.Record, string) moves.Check { |
| return func(ctx context.Context, s release.Store, rec provision.Record, version string) moves.Check { |
| if rec.Move == nil { |
| return moves.Check{Reason: t7Failed} |
| @@ -123,6 +152,21 @@ func heldReleasePublished(writes hostwrite.Config, optDir, dataDir string, pub e |
| logMoveStage(rec.Host, version, err) |
| return c |
| } |
| + if via.Relay != nil { |
| + err := via.publish(ctx, s, rt, rec.Move.ID, heldReleaseHoldOp(rec), box, dataDir, version) |
| + switch { |
| + case errors.Is(err, release.ErrHeldNotProven): |
| + logMoveStage(rec.Host, version, err) |
| + return moves.Check{Reason: t7NotProven} |
| + case err != nil: |
| + logMoveStage(rec.Host, version, err) |
| + return moves.Check{Reason: t7Failed} |
| + } |
| + if c, err = verified(); !c.OK { |
| + logMoveStage(rec.Host, version, err) |
| + } |
| + return c |
| + } |
| work, err := writes.Select(ctx, rec.Host, movesActionHeldRelease) |
| if err != nil { |
| logMoveStage(rec.Host, version, err) |
| @@ -151,6 +195,36 @@ func heldReleasePublished(writes hostwrite.Config, optDir, dataDir string, pub e |
| // seam like heldMaterialize. |
| var heldMaterializeUnder = release.MaterializeHeldUnder |
| +// heldReleaseProbeRelease is the probe owners' Release (movesProbeOwners, |
| +// a landing's and a recovery's probe) under the claim on a box whose agent |
| +// cannot write /opt/carlos (Task 7b). A release already live and proven |
| +// for the row is read, not published again, so a probe restart asks root |
| +// nothing. Otherwise the agent's own held-release selection (work) is |
| +// checked once, so a request is never sent for a selection the agent has |
| +// already lost, and root is asked to publish: root selects held-release |
| +// itself and publishes inside that selection, under the render hash |
| +// journalled for rt's hold op. The probe then passes only on the agent's |
| +// own reading. The work's check grants root nothing. |
| +func heldReleaseProbeRelease(via heldReleaseVia, s release.Store, box, optDir, dataDir string, proof *release.ProofGate) func(ctx context.Context, rt registry.Route, version string, work hostwrite.Work) error { |
| + return func(ctx context.Context, rt registry.Route, version string, work hostwrite.Work) error { |
| + if via.Relay == nil || work == nil { |
| + return hostwrite.ErrUnavailable |
| + } |
| + lock := func(read func() error) error { return withHeldStageLock(dataDir, rt.Host, read) } |
| + err := release.VerifyHeldLive(ctx, s, optDir, rt, version, proof, lock) |
| + if err == nil || !errors.Is(err, release.ErrHeldNotLive) { |
| + return err |
| + } |
| + if err := work.With(ctx, hostwrite.Identity{Release: version}, func() error { return nil }); err != nil { |
| + return err |
| + } |
| + if err := via.publish(ctx, s, rt, "", rt.HoldOp, box, dataDir, version); err != nil { |
| + return err |
| + } |
| + return release.VerifyHeldLive(ctx, s, optDir, rt, version, proof, lock) |
| + } |
| +} |
| + |
| // heldReleaseReady is T7 in production with the claim off (§7.3, §9.2, |
| // round-4 finding 6; heldReleaseHooks), and the configured step's release |
| // then too: the pinned release (the source report's Release) is staged for |