package host import ( "context" "errors" "os" "github.com/carlosframework/platform/internal/caps" "github.com/carlosframework/platform/internal/controlsock" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/rootstate" ) var openClaimUnitBindingsRead = func() (rootstate.Store, error) { return rootstate.Open("/var/lib/carlos-claims/unit-bindings", false, false) } type claimUnitShapeReader func(context.Context, registry.Route, hostbox.Selection) error func claimUnitShapePolicy(route func(string) (*registry.Route, error), read claimUnitShapeReader) hostbox.PolicyRefusal { return func(ctx context.Context, _ *hostclaim.Claim, s hostbox.Selection, _ hostwrite.Identity) error { return claimUnitShapeCheck(ctx, route, read, s, s.Installation()) } } // claimUnitShapeCheck is the unit-shape refusal's decision for selection s. // installation is s.Installation(): whether s is the work an installation // selected for itself (hostbox's installWithWork), the only work that may // go on without a registry row. func claimUnitShapeCheck(ctx context.Context, route func(string) (*registry.Route, error), read claimUnitShapeReader, s hostbox.Selection, installation bool) error { if route == nil || read == nil { return os.ErrPermission } rt, err := route(s.Host) if err != nil { return err } if rt == nil { // A fresh install has no row until registerRemote's second // work.With writes it, and every work.With before that runs this // policy (harness G10, 2026-10-21). Only the installation's own work // may go on without a row. Everything else on a host whose row is // absent (a timer delivering to its cached route, a restart, a // retired or deleted host) refuses as before (fix round 2). The // installation checks an empty route for the host, as // rootClaimRestartShape does: a host with a configured shape or a // saved unit binding still refuses through checkClaimUnitShape // (root's boundInputs in agent mode), and registerRemote refuses to // recreate a row its inventory already records as materialized. if !installation { return hostbox.ErrChanged } rt = ®istry.Route{Host: s.Host} } if rt.Host != s.Host { return hostbox.ErrChanged } return read(ctx, *rt, s) } func checkClaimUnitShape(ctx context.Context, b *claimUnitBindings, shapes map[string]claimUnitShape, rt registry.Route, s hostbox.Selection) error { saved, err := b.read(rt.Host) if errors.Is(err, os.ErrNotExist) { if _, configured := shapes[rt.Host]; configured || rt.Unit != "" || rt.Backing != "" || rt.Tunnel != "" { return hostbox.ErrUnsupported } return nil } if err != nil { return err } if saved.Retiring || saved.Retired || saved.Shape != shapes[rt.Host] || !saved.Shape.matches(rt) { return hostbox.ErrChanged } if s.Host != "" && (saved.Owner.Host != s.Host || saved.Owner.Box != s.Box || saved.Owner.Incarnation != s.Captured.Incarnation || saved.Owner.Stamp != s.Captured.Stamp) { return hostbox.ErrChanged } return ctx.Err() } func withClaimUnitShape(ctx context.Context, rt registry.Route, s hostbox.Selection, fn func(claimUnitBinding) error) error { state, err := openClaimUnitBindingsRead() shapes, configErr := productionClaimUnitShapes() if errors.Is(err, os.ErrNotExist) && rt.Unit == "" && rt.Backing == "" && rt.Tunnel == "" && shapes[rt.Host].Host == "" { return fn(claimUnitBinding{}) } if err != nil { return err } defer state.Close() b := &claimUnitBindings{state: state} if configErr != nil { _, e := b.read(rt.Host) if !errors.Is(e, os.ErrNotExist) || rt.Unit != "" || rt.Backing != "" || rt.Tunnel != "" { return configErr } shapes = map[string]claimUnitShape{} } if err = checkClaimUnitShape(ctx, b, shapes, rt, s); err != nil { return err } saved, err := b.read(rt.Host) if errors.Is(err, os.ErrNotExist) { return fn(claimUnitBinding{}) } if err != nil { return err } return fn(saved) } func productionClaimUnitShapePolicy(mode string, route func(string) (*registry.Route, error)) hostbox.PolicyRefusal { return claimUnitShapePolicy(route, func(ctx context.Context, rt registry.Route, s hostbox.Selection) error { if mode == "agent" { _, err := NewClaimRoleRootClient(controlsock.Path).UnitInputs(ctx, rt) return err } if mode != "adopt" && mode != "instance-control" { return os.ErrPermission } return withClaimUnitShape(ctx, rt, s, func(claimUnitBinding) error { return nil }) }) } func rootClaimUnitInputs(ctx context.Context, rt registry.Route) (*hostbox.UnitLaunchInputs, error) { var out *hostbox.UnitLaunchInputs err := withClaimUnitShape(ctx, rt, hostbox.Selection{}, func(b claimUnitBinding) error { if b == (claimUnitBinding{}) { return nil } p, err := readClaimUnitProgram(b.Shape) if err != nil { return err } out = &hostbox.UnitLaunchInputs{Unit: b.Shape.Unit, Socket: b.Shape.Socket, Addr: b.Shape.Addr, DBPath: b.Shape.DBPath, Program: b.Shape.Program, Args: p.Args, Env: p.Env} return nil }) return out, err } func rootClaimRestartShape(ctx context.Context, host string, rt *registry.Route) error { if rt == nil { rt = ®istry.Route{Host: host} } if rt.Host != host { return os.ErrPermission } return withClaimUnitShape(ctx, *rt, hostbox.Selection{}, func(b claimUnitBinding) error { if b != (claimUnitBinding{}) { return hostbox.ErrUnsupported } return nil }) } func (c rootRestartChecks) checkUnitShape(ctx context.Context, host string, rt *registry.Route) error { if c.unitShape == nil { return nil } legacy, err := c.allowLegacy() if err != nil { return err } if legacy { return nil } return c.unitShape(ctx, host, rt) } func productionClaimCapsOps(o caps.Ops, census func() (bool, error)) caps.Ops { previous := o.CheckHost o.CheckHost = func(host string) error { if census == nil { return os.ErrPermission } legacy, err := census() if err != nil { return err } if !legacy { if err := claimBoundCapsRefusal(host); err != nil { return err } } if previous != nil { return previous(host) } return nil } return o } func claimBoundCapsRefusal(host string) error { shapes, e := productionClaimUnitShapes() if e == nil { if _, bound := shapes[host]; bound { return hostbox.ErrUnsupported } } state, e := openClaimUnitBindingsRead() if e != nil && !errors.Is(e, os.ErrNotExist) { return e } if e == nil { b := &claimUnitBindings{state: state} _, e = b.read(host) state.Close() if e == nil { return hostbox.ErrUnsupported } if !errors.Is(e, os.ErrNotExist) { return e } } return nil }