| 1 | package host |
| 2 | |
| 3 | import ( |
| 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. |
| 39 | type 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 | |
| 46 | var claimMissingHomeLog sync.Once |
| 47 | |
| 48 | var claimMoveSources struct { |
| 49 | sync.Mutex |
| 50 | Source ClaimProductionMoveSources |
| 51 | Parks *claimMoveParkSources |
| 52 | } |
| 53 | |
| 54 | func 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. |
| 71 | type 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. |
| 82 | func 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. |
| 106 | type ProductionClaimOwnerProvider struct{} |
| 107 | |
| 108 | func (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. |
| 247 | func 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). |
| 261 | func 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. |
| 325 | func 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. |
| 334 | type claimLegacyRefusal struct{} |
| 335 | |
| 336 | func (claimLegacyRefusal) err() error { |
| 337 | return &ClaimDependencyError{Contracts: []string{"moves held inputs, publication and regional evidence adapters"}} |
| 338 | } |
| 339 | func (r claimLegacyRefusal) Select(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) (ClaimLaunchInputs, error) { |
| 340 | return ClaimLaunchInputs{}, r.err() |
| 341 | } |
| 342 | func (r claimLegacyRefusal) Materialize(context.Context, hostclaim.Observed, ClaimLaunchInputs, hostwrite.Work) error { |
| 343 | return r.err() |
| 344 | } |
| 345 | func (r claimLegacyRefusal) EmptyAuthority(context.Context, hostclaim.Observed, ClaimLaunchInputs) error { |
| 346 | return r.err() |
| 347 | } |
| 348 | func (r claimLegacyRefusal) Publish(context.Context, hostclaim.Observed, ClaimLaunchInputs) error { |
| 349 | return r.err() |
| 350 | } |
| 351 | func (r claimLegacyRefusal) AdmissionEvidence(context.Context, hostclaim.Observed, hostclaim.Admission, *genselect.Plan) error { |
| 352 | return r.err() |
| 353 | } |
| 354 | func (r claimLegacyRefusal) PublicationEvidence(context.Context, hostclaim.Observed) error { |
| 355 | return r.err() |
| 356 | } |
| 357 | |
| 358 | type productionBoxEvidence struct{ Store release.Store } |
| 359 | |
| 360 | func (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 | |
| 365 | type productionClaimRoster struct { |
| 366 | Directory hostactors.Directory |
| 367 | DataBucket, FloorDir string |
| 368 | } |
| 369 | |
| 370 | func (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 | |
| 405 | type productionClaimRegional struct { |
| 406 | Now func() time.Time |
| 407 | Identity hostbox.IdentityProvider |
| 408 | Parks *claimMoveParkSources |
| 409 | Local *productionClaimLocal |
| 410 | } |
| 411 | |
| 412 | func (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 | } |
| 445 | func (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 | } |
| 455 | func (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 | } |
| 461 | func (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. |
| 505 | type 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 | |
| 514 | func (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). |
| 560 | func 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. |
| 604 | func 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 | |