carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "crypto/ecdh"
6 "crypto/ed25519"
7 "errors"
8 "log"
9 "os"
10 "path/filepath"
11 "runtime"
12 "strings"
13 "sync"
14 "time"
15
16 "github.com/carlosframework/platform/internal/activator"
17 "github.com/carlosframework/platform/internal/appconfig"
18 "github.com/carlosframework/platform/internal/boxid"
19 "github.com/carlosframework/platform/internal/controlsock"
20 "github.com/carlosframework/platform/internal/genselect"
21 "github.com/carlosframework/platform/internal/hostactors"
22 "github.com/carlosframework/platform/internal/hostbox"
23 "github.com/carlosframework/platform/internal/hostclaim"
24 "github.com/carlosframework/platform/internal/hostfloor"
25 "github.com/carlosframework/platform/internal/hostguard"
26 "github.com/carlosframework/platform/internal/hostops"
27 "github.com/carlosframework/platform/internal/hostwrite"
28 "github.com/carlosframework/platform/internal/lease"
29 "github.com/carlosframework/platform/internal/moves"
30 "github.com/carlosframework/platform/internal/park"
31 "github.com/carlosframework/platform/internal/registry"
32 "github.com/carlosframework/platform/internal/release"
33 "github.com/carlosframework/platform/internal/version"
34)
35
36// ClaimProductionMoveSources are the moves-owned C8 and regional contracts.
37// They do not grant local process or roster authority. Group D's public owner
38// and OperationAttempts signatures are unchanged.
39type ClaimProductionMoveSources interface {
40 Open(context.Context, release.Store, lease.VersionedStore, hostbox.Actor, ClaimMoveReadAccess) (ClaimMovesOwners, ClaimFilesManifest, error)
41 View(context.Context, hostclaim.Observed) (park.View, error)
42 VerifyRetirement(context.Context, hostclaim.Observed, []lease.ObjectVersion) (string, time.Time, error)
43 Close(context.Context) error
44}
45
46var claimMissingHomeLog sync.Once
47
48var claimMoveSources struct {
49 sync.Mutex
50 Source ClaimProductionMoveSources
51 Parks *claimMoveParkSources
52}
53
54func RegisterProductionClaimMoveSources(s ClaimProductionMoveSources) error {
55 claimMoveSources.Lock()
56 defer claimMoveSources.Unlock()
57 if s == nil || claimMoveSources.Source != nil {
58 return hostwrite.ErrUnavailable
59 }
60 if claimMoveSources.Parks == nil {
61 claimMoveSources.Parks = &claimMoveParkSources{View: s.View, Evidence: s}
62 }
63 claimMoveSources.Source = s
64 return nil
65}
66
67// productionRosterRow is this box's verified roster row and what read it:
68// the release key, the box's host-actor enrollment, the signed directory,
69// and the directory's row for the box, whose enrollment and cloud instance
70// are the enrollment's.
71type productionRosterRow struct {
72 home string
73 pub ed25519.PublicKey
74 enrollment hostfloor.Enrollment
75 directory hostactors.Directory
76 row hostactors.Box
77}
78
79// readProductionRosterRow is OpenClaimOwners' roster read, shared with the
80// moves park view's region (productionRosterRegion, Plan 5 Task 9a fix
81// round 1) so the two cannot disagree.
82func readProductionRosterRow(ctx context.Context, store release.Store, getenv func(string) string) (productionRosterRow, error) {
83 var rr productionRosterRow
84 pub, err := release.ParsePublicKey(getenv("CARLOS_RELEASE_PUBKEY"), "CARLOS_RELEASE_PUBKEY")
85 if err != nil {
86 return rr, err
87 }
88 enrollment, err := hostfloor.ReadEnrollment("/etc/carlos/host-actor.json")
89 if err != nil {
90 return rr, err
91 }
92 directory := hostactors.Directory{Store: store, PublicKey: pub, Bucket: getenv("CARLOS_DEPLOYMENT_BUCKET")}
93 roster, err := directory.Read(ctx)
94 if err != nil {
95 return rr, err
96 }
97 row, ok := roster.Roster.Boxes[enrollment.Box]
98 if !ok || row.Enrollment != enrollment.Enrollment || row.Cloud.InstanceID != enrollment.InstanceID {
99 return rr, hostbox.ErrChanged
100 }
101 return productionRosterRow{home: roster.Roster.HomeRegion, pub: pub, enrollment: enrollment, directory: directory, row: row}, nil
102}
103
104// ProductionClaimOwnerProvider constructs claim-owned authority directly. Only
105// C8 and the moves regional snapshot/lease inventory come from the moves owner.
106type ProductionClaimOwnerProvider struct{}
107
108func (ProductionClaimOwnerProvider) OpenClaimOwners(ctx context.Context, store release.Store, r ClaimRuntimeResources) (ClaimOwnerBindings, error) {
109 var out ClaimOwnerBindings
110 if store == nil || r.Registry == nil || (r.Mode != "agent" && r.Mode != "adopt" && r.Mode != "instance-control") {
111 return out, hostwrite.ErrUnavailable
112 }
113 rr, err := readProductionRosterRow(ctx, store, os.Getenv)
114 if err != nil {
115 return out, err
116 }
117 pub, enrollment, directory, row := rr.pub, rr.enrollment, rr.directory, rr.row
118 certs, err := boxid.EmbeddedCerts()
119 if err != nil {
120 return out, err
121 }
122 dataBucket := os.Getenv("CARLOS_HIBERNATE_BUCKET")
123 if dataBucket == "" {
124 return out, hostwrite.ErrUnavailable
125 }
126 authority := productionClaimRoster{Directory: directory, DataBucket: dataBucket, FloorDir: filepath.Join(r.OptDir, "system", "guard")}
127 _, homeRegion, err := claimRegionBinding(row.Region, os.Getenv("CARLOS_REGION"), rr.home)
128 if err != nil {
129 return out, err
130 }
131 if rr.home == "" {
132 claimMissingHomeLog.Do(func() {
133 log.Printf("host claim: signed roster has no home region; running with region %q and no home alias", row.Region)
134 })
135 }
136 out.Identity = hostbox.IdentityProvider{HomeRegion: homeRegion, AWSRegion: row.Cloud.Region, Account: row.Cloud.Account, Box: row.BoxID, Region: row.Region, Bucket: dataBucket, Namespace: row.Namespace, Certs: certs, Roster: authority, Evidence: productionBoxEvidence{store}}
137 var actor hostbox.Actor
138 if r.Mode == "instance-control" {
139 out.cleanupEnrollment, err = verifyClaimCleanupEnrollment(ctx, out.Identity, directory)
140 if err == nil {
141 actor = out.cleanupEnrollment.actor
142 }
143 } else {
144 actor, err = out.Identity.Verify(ctx)
145 }
146 if err != nil {
147 return out, err
148 }
149 if actor.Enrollment != enrollment.Enrollment || actor.Cloud.InstanceID != enrollment.InstanceID {
150 return out, hostbox.ErrChanged
151 }
152 running, err := (hostfloor.Disk{Dir: authority.FloorDir, Bucket: directory.Bucket, PublicKey: pub}).Running(version.Build, runtime.GOOS+"-"+runtime.GOARCH)
153 if err != nil || !running.GuardReady {
154 return out, errors.Join(err, hostfloor.ErrFloor)
155 }
156 for _, dir := range []string{filepath.Join(r.DataDir, ".hostexec"), "/run/carlos/hostexec"} {
157 if err = prepareClaimParticipationDirectory(dir); err != nil {
158 return out, err
159 }
160 }
161 out.RetirementAuthority = &hostops.ProductionAuthority{Store: store, Directory: directory, Now: time.Now}
162 if r.Mode == "instance-control" {
163 out.EvidenceKey, err = readClaimEvidenceKey("/etc/carlos/host-evidence.key")
164 if err != nil {
165 return out, err
166 }
167 }
168 out.Controllers = productionStopControllers{}
169 out.Data, err = lease.NewVersionedS3(ctx, map[string]string{dataBucket: row.Cloud.Region})
170 if err != nil {
171 return out, err
172 }
173 marker, err := newProductionClaimMarkers(ctx, dataBucket, row.Cloud.Region)
174 if err != nil {
175 return out, err
176 }
177 local := &productionClaimLocal{Resources: r, Identity: out.Identity, Directory: directory, Store: store, slots: map[string]*claimParticipation{}}
178 out.Local = local
179 backend := claimExecBackend(r, productionInstanceKeyReaders())
180 // The token mint is the only write this source makes. It runs under the
181 // same guard and claim store the assembly's WorkSource uses.
182 tokens := claimAdminTokens{Guard: hostguard.Guard{DataDir: r.DataDir, Claims: hostguard.StoreReader{Store: store}}, Claims: store, Verify: out.Identity.Verify, Local: out.Identity.Local(local), Policy: hostbox.Policy{Held: hostbox.RegistryHoldPolicy(r.Registry)}.Check, Now: time.Now}
183 unitInputs := claimUnitInputReader(NewClaimRoleRootClient(controlsock.Path).UnitInputs)
184 if r.Mode != "agent" {
185 unitInputs = rootClaimUnitInputs
186 }
187 out.Serving.Launch = claimServingLaunchSource(backend, tokens, unitInputs)
188 out.Serving.Markers = marker
189 moveWakeEnabled := hostwrite.Enabled(claimEntryEnv(ctx)) && moves.Enabled(claimEntryEnv(ctx))
190 if moveWakeEnabled {
191 out.moveRead = &claimMoveReadAccess{check: claimBoundIdentity(out.Identity.Verify, actor), actor: actor, route: r.Registry.Get, launch: claimMoveLaunchSource(backend)}
192 }
193 claimMoveSources.Lock()
194 external := claimMoveSources.Source
195 parks := claimMoveSources.Parks
196 claimMoveSources.Unlock()
197 if moveWakeEnabled {
198 openProductionClaimMoveWake(ctx, store, actor, external, &out, claimBoundIdentity(out.Identity.Verify, actor))
199 }
200 // The C8 files manifest provider (moves Task 6; platform-1e ruling of
201 // 2026-10-12): registered under CARLOS_HOST_CLAIM alone, apart from the
202 // moves owners, because Establish runs on every claim-on serving wake.
203 closeFiles, err := openClaimFilesManifest(ctx, os.Getenv, productionClaimFilesSource(), store, out.Data, actor, &out, claimBoundIdentity(out.Identity.Verify, actor))
204 if err != nil {
205 return out, err
206 }
207 // Region HA's relocation owners (claim Plan 11), with CARLOS_REGION_HA on.
208 closeRelocation, err := openClaimRelocationOwners(ctx, os.Getenv, productionClaimRelocationSource(), store, out.Data, actor, &out)
209 if err != nil {
210 if closeFiles != nil {
211 err = errors.Join(err, closeFiles(ctx))
212 }
213 return out, err
214 }
215 bindClaimLegacyOwners(&out)
216 out.InputPolicy = claimInputPolicy(r.Registry.Get, store, pub, backend, unitInputs)
217 out.PolicyRefusals = append(out.PolicyRefusals, productionClaimUnitShapePolicy(r.Mode, r.Registry.Get))
218 out.MovesPolicy = MovesClaimPolicy(MovesRecordsFrom(store))
219 bindProductionClaimInputPolicy(claimEntryEnv(ctx), &out, MovesRecordsFrom(store))
220 regional := productionClaimRegional{Identity: out.Identity, Parks: parks, Local: local}
221 out.Evidence = regional
222 out.LeaseSet = regional.LeaseSet
223 out.View = regional.View
224 out.Authorize = regional.Authorize
225 out.Close = func(ctx context.Context) error {
226 err := local.Close(ctx)
227 if closeRelocation != nil {
228 err = errors.Join(err, closeRelocation(ctx))
229 }
230 if closeFiles != nil {
231 err = errors.Join(err, closeFiles(ctx))
232 }
233 if external != nil && moveWakeEnabled {
234 return errors.Join(err, external.Close(ctx))
235 }
236 return err
237 }
238 return out, nil
239}
240
241// claimExecBackend is the claim path's launch backend: the box's
242// directories and cgroups, and the instance-key readers every launching
243// backend needs (final review I1). Without them a claim launch refused
244// every keyed replica ("cannot be checked against its home's key") and
245// could mint before the configuration was delivered; the agent's backend
246// is wired from the same pair.
247func claimExecBackend(r ClaimRuntimeResources, keys instanceKeyReaders) *activator.ExecBackend {
248 eb := &activator.ExecBackend{DataDir: r.DataDir, OptDir: r.OptDir, Cgroups: r.Manager}
249 keys.wire(eb)
250 return eb
251}
252
253// claimInputPolicy is the serving claim's input policy: does a Work's
254// identity still name this host's inputs? A config-only identity is checked
255// against the bucket's rendered config, a release-only one against the
256// route's release, and a full one against the launch snapshot's identity.
257// The snapshot is the gateless read (SnapshotClaimLaunch): nothing is
258// launched here, so the replica gate does not run, no bucket is read for a
259// configured key, and the answer is hostbox.ErrChanged or nil, never a
260// launch refusal (final review I4).
261func claimInputPolicy(route func(string) (*registry.Route, error), store release.Store, pub ed25519.PublicKey, backend *activator.ExecBackend, readers ...claimUnitInputReader) func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error {
262 return func(ctx context.Context, c *hostclaim.Claim, s hostbox.Selection, id hostwrite.Identity) error {
263 if c == nil || c.Holder == nil || c.Op.Kind != hostclaim.KindServing || s.Host != c.Host || s.Captured.Incarnation != c.Holder.Incarnation || s.Captured.Stamp != c.Holder.Stamp {
264 return hostbox.ErrChanged
265 }
266 rt, err := route(s.Host)
267 if err != nil {
268 return err
269 }
270 if rt == nil {
271 return hostbox.ErrChanged
272 }
273 var unit *hostbox.UnitLaunchInputs
274 if len(readers) == 1 {
275 unit, err = readers[0](ctx, *rt)
276 if err != nil {
277 return err
278 }
279 }
280 if unit != nil {
281 if id.Config != "" && id.Release == "" {
282 if id.Config != unit.Program {
283 return hostbox.ErrChanged
284 }
285 return nil
286 }
287 if id.Release != "" && id.Config == "" {
288 return release.CheckRouteReleaseIdentity(ctx, store, pub, *rt, id)
289 }
290 snapshot, err := claimBoundSnapshot(ctx, backend, *rt, func(context.Context, registry.Route) (*hostbox.UnitLaunchInputs, error) { return unit, nil })
291 if err != nil {
292 return err
293 }
294 if id != snapshot.Identity {
295 return hostbox.ErrChanged
296 }
297 return nil
298 }
299 if id.Config != "" && id.Release == "" {
300 var private *ecdh.PrivateKey
301 if raw := os.Getenv("CARLOS_SECRETS_KEY"); raw != "" {
302 private, err = appconfig.ParsePrivateKey(raw, "CARLOS_SECRETS_KEY")
303 if err != nil {
304 return err
305 }
306 }
307 return appconfig.CheckRenderedIdentity(ctx, store, private, *rt, id)
308 }
309 if id.Release != "" && id.Config == "" {
310 return release.CheckRouteReleaseIdentity(ctx, store, pub, *rt, id)
311 }
312 snapshot, err := backend.SnapshotClaimLaunch(ctx, *rt)
313 if err != nil {
314 return err
315 }
316 if id != snapshot.Identity {
317 return hostbox.ErrChanged
318 }
319 return nil
320 }
321}
322
323// bindClaimLegacyOwners fills the claims-off fallback. Claimed serving uses
324// out.Serving independently of moves and never dispatches through these fields.
325func bindClaimLegacyOwners(out *ClaimOwnerBindings) {
326 legacy := claimLegacyRefusal{}
327 out.Inputs = legacy
328 out.AdmissionEvidence = legacy.AdmissionEvidence
329 out.PublicationEvidence = legacy.PublicationEvidence
330}
331
332// claimLegacyRefusal is main's refusal for the held inputs, publication and
333// regional evidence adapters it never had.
334type claimLegacyRefusal struct{}
335
336func (claimLegacyRefusal) err() error {
337 return &ClaimDependencyError{Contracts: []string{"moves held inputs, publication and regional evidence adapters"}}
338}
339func (r claimLegacyRefusal) Select(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) (ClaimLaunchInputs, error) {
340 return ClaimLaunchInputs{}, r.err()
341}
342func (r claimLegacyRefusal) Materialize(context.Context, hostclaim.Observed, ClaimLaunchInputs, hostwrite.Work) error {
343 return r.err()
344}
345func (r claimLegacyRefusal) EmptyAuthority(context.Context, hostclaim.Observed, ClaimLaunchInputs) error {
346 return r.err()
347}
348func (r claimLegacyRefusal) Publish(context.Context, hostclaim.Observed, ClaimLaunchInputs) error {
349 return r.err()
350}
351func (r claimLegacyRefusal) AdmissionEvidence(context.Context, hostclaim.Observed, hostclaim.Admission, *genselect.Plan) error {
352 return r.err()
353}
354func (r claimLegacyRefusal) PublicationEvidence(context.Context, hostclaim.Observed) error {
355 return r.err()
356}
357
358type productionBoxEvidence struct{ Store release.Store }
359
360func (e productionBoxEvidence) Load(ctx context.Context, box string) ([]byte, error) {
361 raw, _, err := e.Store.Get(ctx, boxid.Key(box))
362 return raw, err
363}
364
365type productionClaimRoster struct {
366 Directory hostactors.Directory
367 DataBucket, FloorDir string
368}
369
370func (p productionClaimRoster) Actor(ctx context.Context, box string) (hostbox.Actor, error) {
371 var a hostbox.Actor
372 o, err := p.Directory.Read(ctx)
373 if err != nil {
374 return a, err
375 }
376 b, ok := o.Roster.Boxes[box]
377 if !ok || b.State != "active" || o.Roster.RequestLimitNS != int64(hostbox.RequestLimit) {
378 return a, hostbox.ErrChanged
379 }
380 fenced, err := p.Directory.TerminationFence(ctx, o, box, time.Now())
381 if err != nil || fenced {
382 return a, errors.Join(err, hostbox.ErrChanged)
383 }
384 disk := hostfloor.Disk{Dir: p.FloorDir, Bucket: p.Directory.Bucket, PublicKey: p.Directory.PublicKey}
385 _, local, err := disk.Load()
386 if err != nil {
387 return a, err
388 }
389 remote, err := hostfloor.Read(ctx, p.Directory.Store, p.Directory.PublicKey, p.Directory.Bucket)
390 if err != nil {
391 return a, err
392 }
393 if local.Digest != remote.Digest || o.Roster.Publication != "published" || o.Roster.FloorSHA256 != remote.Digest {
394 return a, hostfloor.ErrFloor
395 }
396 // The binding comes from this same verified read, so it names exactly the
397 // row and deployment facts checked above.
398 binding, ok := hostactors.BoxBinding(o.Roster, box)
399 if !ok {
400 return a, hostbox.ErrChanged
401 }
402 return hostbox.Actor{FleetHomeRegion: o.Roster.HomeRegion, ControlBucket: p.Directory.Bucket, Box: box, Enrollment: b.Enrollment, Revision: o.Roster.Revision, Region: b.Region, Bucket: p.DataBucket, Namespace: b.Namespace, Eligible: true, Binding: binding, Cloud: hostbox.CloudIdentity{Provider: b.Cloud.Provider, Account: b.Cloud.Account, Region: b.Cloud.Region, InstanceID: b.Cloud.InstanceID}}, nil
403}
404
405type productionClaimRegional struct {
406 Now func() time.Time
407 Identity hostbox.IdentityProvider
408 Parks *claimMoveParkSources
409 Local *productionClaimLocal
410}
411
412func (p productionClaimRegional) View(ctx context.Context, o hostclaim.Observed) (park.View, error) {
413 if p.Parks == nil {
414 return p.ordinaryView(ctx, o)
415 }
416 if p.Parks.View == nil {
417 return park.View{}, &ClaimDependencyError{Contracts: []string{"moves Plan 5 exact-version generation and files-manifest view"}}
418 }
419 actor, err := p.Identity.Verify(ctx)
420 if err != nil {
421 return park.View{}, err
422 }
423 if o.Claim == nil {
424 return park.View{}, park.ErrUnsafe
425 }
426 home, err := park.VerifiedHome(actor.FleetHomeRegion, actor.HomeRegion, actor.Region, o.Claim.Op.Intent["home_region"])
427 if err != nil {
428 return park.View{}, err
429 }
430 v, err := p.Parks.View(ctx, park.HomeView(o, home))
431 if err != nil {
432 return park.View{}, err
433 }
434 v.HomeRegion = home
435 v.ArchivedIncarnations = claimArchivedLocations(o.Claim)
436 v.RefusedLocations = map[string]error{}
437 if _, err := claimLegacyIncarnation(o.Claim); err != nil {
438 v.RefusedLocations["instances/"+o.Host] = err
439 }
440 if o.Claim.Holder == nil && len(o.Claim.RetiredIncarnations) == 0 {
441 v.RefusedLocations["moves/"+o.Host] = claimRetirementDependency()
442 }
443 return v, nil
444}
445func (p productionClaimRegional) LeaseSet(ctx context.Context, bucket, scope string) ([]string, error) {
446 a, err := p.Identity.Verify(ctx)
447 if err != nil {
448 return nil, err
449 }
450 if a.Bucket != bucket {
451 return nil, hostbox.ErrChanged
452 }
453 return hostbox.ReplicaParkLeaseSet(ctx, bucket, scope)
454}
455func (p productionClaimRegional) VerifyRetirement(ctx context.Context, o hostclaim.Observed, v []lease.ObjectVersion) (string, time.Time, error) {
456 if _, err := p.Identity.Verify(ctx); err != nil {
457 return "", time.Time{}, err
458 }
459 return p.retirement(ctx, o, v)
460}
461func (p productionClaimRegional) Authorize(ctx context.Context, o hostclaim.Observed, ob hostclaim.Obligation) error {
462 a, err := p.Identity.Verify(ctx)
463 if err != nil {
464 return err
465 }
466 ordinary := p.Parks == nil && ob.Kind == "park-generation" && strings.HasPrefix(ob.Op, "session:")
467 if ordinary {
468 if _, err := p.VerifyObligation(ctx, o, ob); err != nil {
469 return err
470 }
471 }
472 if o.Claim == nil || (!ordinary && ob.Op != o.Claim.Op.ID) || (ob.Owner != "box:"+a.Box && (ob.Owner != "region:"+ob.Region || !park.SameRegion(a.HomeRegion, ob.Region, a.Region))) || (ob.Box != "" && ob.Box != a.Box) || (ob.Region != "" && !park.SameRegion(a.HomeRegion, ob.Region, a.Region)) {
473 return hostbox.ErrChanged
474 }
475 found := false
476 for _, live := range o.Claim.Obligations {
477 if live.ID == ob.ID && live.Owner == ob.Owner && live.Op == ob.Op {
478 found = true
479 }
480 }
481 if !found {
482 return hostbox.ErrChanged
483 }
484 if ordinary {
485 return nil
486 }
487 if p.Parks == nil && ob.Owner == "region:"+ob.Region && p.Local != nil && p.Local.assembly != nil && (p.Local.assembly.Guard.Root != nil || p.Local.assembly.Guard.FenceView != nil) {
488 return p.authorizeOrdinaryRetirement(ctx, o, ob)
489 }
490 return p.Local.Prepare(ctx, o)
491}
492
493// claimAdminTokens authorizes a first wake's CARLOS_ADMIN_TOKEN mint. The
494// legacy launch preparation mints it before reading the env. The claim path
495// never runs that preparation, and SnapshotClaimLaunch refuses a missing
496// token, so selection would refuse every first wake. The mint is a host
497// write, so it runs only under the claim's current serving holder for this
498// box. The checks are the ones a selected "spawn" Work makes (hostbox
499// WorkSource): exact op, holder stamp, session and generation; quiet-after;
500// local authority; and the serving policy with a zero identity. Before
501// Admit there is no local hostguard record, so the write runs under
502// Guard.WithNoInstallation. After Admit it runs under Guard.With at the
503// holder's captured stamp. A record for any other incarnation or stamp
504// refuses the write.
505type claimAdminTokens struct {
506 Guard hostguard.Guard
507 Claims release.Store
508 Verify func(context.Context) (hostbox.Actor, error)
509 Local func(context.Context, *hostclaim.Claim, hostbox.Selection) error
510 Policy func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error
511 Now func() time.Time
512}
513
514func (t claimAdminTokens) With(ctx context.Context, host string, effect func() error) error {
515 if ctx == nil || effect == nil || t.Guard.DataDir == "" || t.Guard.Claims == nil || t.Claims == nil || t.Verify == nil || t.Local == nil || t.Policy == nil || t.Now == nil {
516 return hostwrite.ErrUnavailable
517 }
518 o, err := hostclaim.Read(ctx, t.Claims, host)
519 if err != nil {
520 return err
521 }
522 if o.Absent || o.Claim == nil || o.Claim.Holder == nil || o.Claim.Host != host || o.Claim.Op.Kind != hostclaim.KindServing || o.Claim.Holder.Stamp == 0 {
523 return hostbox.ErrChanged
524 }
525 actor, err := t.Verify(ctx)
526 if err != nil {
527 return err
528 }
529 c, h := o.Claim, o.Claim.Holder
530 sel := hostbox.Selection{HomeRegion: actor.HomeRegion, FleetHomeRegion: actor.FleetHomeRegion, Host: host, Action: "spawn", Box: actor.Box, Region: actor.Region, Captured: hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}, Kind: c.Op.Kind, Op: c.Op.ID, Phase: c.Op.Phase, Generation: h.Generation, Session: h.Session}
531 check := func(c *hostclaim.Claim) error {
532 if c.Op.Kind != sel.Kind || c.Op.ID != sel.Op || c.Op.Phase != sel.Phase || c.Holder.Incarnation != sel.Captured.Incarnation || c.Holder.Stamp != sel.Captured.Stamp || c.Holder.Session != sel.Session || c.Holder.Generation != sel.Generation {
533 return hostbox.ErrChanged
534 }
535 if until := park.QuietUntil(c.QuietAfter, actor.HomeRegion, sel.Region); !until.IsZero() && !t.Now().After(until) {
536 return hostbox.ErrChanged
537 }
538 if err := t.Local(ctx, c, sel); err != nil {
539 return err
540 }
541 return t.Policy(ctx, c, sel, hostwrite.Identity{})
542 }
543 captured, err := t.Guard.Capture(host)
544 if errors.Is(err, hostguard.ErrNoRecord) {
545 return t.Guard.WithNoInstallation(ctx, host, sel.Captured.Incarnation, check, effect)
546 }
547 if err != nil {
548 return err
549 }
550 if captured != sel.Captured {
551 return hostbox.ErrChanged
552 }
553 return t.Guard.With(ctx, host, captured, check, effect)
554}
555
556// claimServingLaunchSource is the production serving launch source: the
557// authorized token initialization, then the read-only snapshot with the
558// replica gate (SelectClaimLaunch), then a declined box's launch gate
559// (declinedClaimGate).
560func claimServingLaunchSource(backend *activator.ExecBackend, tokens claimAdminTokens, readers ...claimUnitInputReader) func(context.Context, registry.Route) (ClaimServingLaunch, error) {
561 return func(ctx context.Context, rt registry.Route) (ClaimServingLaunch, error) {
562 if len(readers) != 0 {
563 unit, err := readers[0](ctx, rt)
564 if err != nil {
565 return ClaimServingLaunch{}, err
566 }
567 if unit != nil {
568 return claimBoundSnapshot(ctx, backend, rt, func(context.Context, registry.Route) (*hostbox.UnitLaunchInputs, error) { return unit, nil })
569 }
570 }
571 if err := ensureClaimLaunchToken(ctx, backend, rt, func(ctx context.Context, effect func() error) error {
572 if layout, _ := backend.Layout(rt); layout == activator.LayoutNested {
573 if err := tokens.prepareState(ctx, rt); err != nil {
574 return err
575 }
576 }
577 return tokens.With(ctx, rt.Host, effect)
578 }); err != nil {
579 return ClaimServingLaunch{}, err
580 }
581 snapshot, err := backend.SelectClaimLaunch(ctx, rt)
582 // snapshot.InstanceKeySHA256 and InstanceKeySource are dropped here
583 // until O6 threads them to the launch's adoption verb (plan ruling
584 // 23; O6's obligation): a claim launch binds no control socket yet,
585 // so nothing can report against them.
586 if err == nil {
587 err = declinedClaimGate(backend.OptDir, rt, snapshot)
588 }
589 return ClaimServingLaunch{Identity: snapshot.Identity, Executable: snapshot.Executable, ExecutableSHA256: snapshot.SHA256, Args: snapshot.Args, Env: snapshot.Env, Roles: snapshot.Roles, Version: snapshot.Version, Staged: snapshot.Staged, Limit: snapshot.Limit}, err
590 }
591}
592
593// declinedClaimGate is a declined box's launch gate (failureHolds.gate) for
594// a claims-backed wake (Task 9, plan review #6). Task 1 makes a claims-on
595// box decline CARLOS_SWAP=overlap, so this is a declined box's wake, and its
596// launch runs the bytes the snapshot hashed from the live link (the launch
597// hashes the executable again before it execs). The snapshot names the
598// version from a second read of the link, so the link is resolved once more
599// here (release.ResolveLive, one read of the link and a hash of the file it
600// names): only a version whose bytes are the snapshot's is gated, and a
601// repoint between the snapshot's two reads is a changed claim
602// (hostbox.ErrChanged), never a start. With nothing honoured (every box but
603// a declined one with records or markers) it is nil and reads nothing.
604func declinedClaimGate(optDir string, rt registry.Route, s activator.ClaimLaunchSnapshot) error {
605 h := heldHolds()
606 if h == nil {
607 return nil
608 }
609 id, err := release.ResolveLive(optDir, rt.Host)
610 if err != nil {
611 return err
612 }
613 if id.Version != s.Version || !strings.EqualFold(id.Digest, s.SHA256) {
614 return hostbox.ErrChanged
615 }
616 return h.gate(rt, s.Version)
617}
618