// Package runtime implements the FLUID deterministic data plane: the gateway, // revision resolution, routing, backend connection and telemetry emission. // // Nothing here may depend on the evolution control plane to serve a request. // ArchitectureBlueprint.md section 2 makes this an invariant: the interface // runtime must continue to function when the Daimon, the model provider, the // hypothesis store, the experiment controller and the AI budget are all // unavailable. package runtime import ( "errors" "fmt" "sync" "github.com/tegwick/fluid-core/internal/contract" "github.com/tegwick/fluid-core/internal/signing" ) // Registry is the gateway's cached view of published control-plane state. // // It is deliberately a snapshot rather than a client. When the control plane // dies the registry keeps answering from what it last held, which is what // ArchitectureBlueprint.md section 34.6 requires: the runtime continues using // cached published configuration, and no new promotions occur until // control-plane consistency is restored. type Registry struct { mu sync.RWMutex iface contract.InterfaceID revisions map[contract.RevisionID]contract.Revision policy contract.RoutingPolicy hasPolicy bool // verifier, when set, is applied to every descriptor before it is accepted. verifier *signing.Verifier } // NewRegistry returns a registry that accepts unsigned descriptors. // // This is for development and tests. A deployment that routes real traffic // should use NewVerifiedRegistry: ArchitectureBlueprint.md section 35 requires // the router to accept only signed published descriptors, and a registry that // takes anything makes the whole verification pipeline optional. func NewRegistry(iface contract.InterfaceID) *Registry { return &Registry{ iface: iface, revisions: make(map[contract.RevisionID]contract.Revision), } } // NewVerifiedRegistry returns a registry that refuses any descriptor which does // not carry a signature from a trusted key. func NewVerifiedRegistry(iface contract.InterfaceID, v *signing.Verifier) *Registry { r := NewRegistry(iface) r.verifier = v return r } var ( // ErrUnknownRevision is returned for a revision the registry has never seen. ErrUnknownRevision = errors.New("unknown revision") // ErrRevisionNotRoutable is returned for a revision that exists but must not // receive traffic. ErrRevisionNotRoutable = errors.New("revision not routable") // ErrNoPolicy is returned before any routing policy has been loaded. ErrNoPolicy = errors.New("no routing policy loaded") // ErrWrongInterface guards against loading another interface's artifacts. ErrWrongInterface = errors.New("artifact belongs to a different interface") // ErrStalePolicy is returned when an older policy generation is offered. ErrStalePolicy = errors.New("routing policy generation is not newer") ) // PutRevision publishes a revision descriptor into the registry. // // The descriptor is expected to have been signature-verified already; this // method enforces only the structural conditions the router depends on. func (r *Registry) PutRevision(d contract.Revision) error { if d.Interface != r.iface { return fmt.Errorf("%w: descriptor is for %q, registry serves %q", ErrWrongInterface, d.Interface, r.iface) } if d.Runtime.Upstream == "" { return fmt.Errorf("revision %s: descriptor has no runtime upstream", d.ID) } if !d.State.Valid() { return fmt.Errorf("revision %s: unknown state %q", d.ID, d.State) } if r.verifier != nil { if err := r.verifyDescriptor(d); err != nil { return fmt.Errorf("revision %s: %w", d.ID, err) } } r.mu.Lock() defer r.mu.Unlock() r.revisions[d.ID] = d return nil } // Revision returns a published descriptor. func (r *Registry) Revision(id contract.RevisionID) (contract.Revision, error) { r.mu.RLock() defer r.mu.RUnlock() d, ok := r.revisions[id] if !ok { return contract.Revision{}, fmt.Errorf("%w: %s", ErrUnknownRevision, id) } return d, nil } // PutPolicy installs a routing policy. // // Generations are monotonic: an older policy is refused rather than applied. // Without this a delayed delivery could silently roll traffic back to a // superseded allocation, which would corrupt an in-flight experiment's // measurement window. func (r *Registry) PutPolicy(p contract.RoutingPolicy) error { if p.Interface != r.iface { return fmt.Errorf("%w: policy is for %q, registry serves %q", ErrWrongInterface, p.Interface, r.iface) } r.mu.Lock() defer r.mu.Unlock() if r.hasPolicy && p.Generation <= r.policy.Generation { return fmt.Errorf("%w: offered %d, holding %d", ErrStalePolicy, p.Generation, r.policy.Generation) } r.policy = p r.hasPolicy = true return nil } // Policy returns the current routing policy. func (r *Registry) Policy() (contract.RoutingPolicy, error) { r.mu.RLock() defer r.mu.RUnlock() if !r.hasPolicy { return contract.RoutingPolicy{}, ErrNoPolicy } return r.policy, nil } // routableStates lists the descriptor states the router may send traffic to. // // ArchitectureBlueprint.md section 5.3 requires rejecting unpublished, failed // and retired revisions. "created" is unpublished; "verified" has passed tests // but has not been exposed; "retired" is finished. var routableStates = map[contract.RevisionState]bool{ contract.RevisionStateExperiment: true, contract.RevisionStateCandidate: true, contract.RevisionStateStable: true, contract.RevisionStateDeprecated: true, } // CheckRoutable reports whether a revision may currently receive traffic from // the given cohort. func (r *Registry) CheckRoutable(id contract.RevisionID, cohort contract.CohortID) error { d, err := r.Revision(id) if err != nil { return err } if !routableStates[d.State] { return fmt.Errorf("%w: %s is %s", ErrRevisionNotRoutable, id, d.State) } if d.Policy.SecurityCheck != contract.RevisionPolicySecurityCheckPassed { return fmt.Errorf("%w: %s has security_check=%s", ErrRevisionNotRoutable, id, d.Policy.SecurityCheck) } if d.Policy.PolicyCheck != nil && *d.Policy.PolicyCheck == contract.RevisionPolicyPolicyCheckFailed { return fmt.Errorf("%w: %s failed its policy check", ErrRevisionNotRoutable, id) } if d.Routing != nil && len(d.Routing.EligibleCohorts) > 0 { if !containsCohort(d.Routing.EligibleCohorts, cohort) { return fmt.Errorf("%w: cohort %q is not eligible for %s", ErrRevisionNotRoutable, cohort, id) } } return nil } // verifyDescriptor checks a descriptor's signature. // // The descriptor is verified in the exact form it was signed: the document // wrapper included, the signature member excluded. Anything else would let a // re-wrapped descriptor pass under a signature made for different bytes. func (r *Registry) verifyDescriptor(d contract.Revision) error { if d.Signature == nil { return signing.ErrUnsigned } return r.verifier.Verify( contract.RevisionDescriptorDocument{Revision: d}, &signing.Signature{ Algorithm: string(d.Signature.Algorithm), KeyID: d.Signature.KeyID, Value: d.Signature.Value, }, ) } func containsCohort(list []contract.CohortID, want contract.CohortID) bool { for _, c := range list { if c == want { return true } } return false }