1package ports
2
3import (
4 "errors"
5 "fmt"
6 "time"
7
8 "github.com/bnema/vev/internal/domain"
9 "github.com/bnema/vev/internal/protocol/catalogue"
10)
11
12// Broker-native daemon observation.
13//
14// BrokerDaemonObservation is the broker's own projection of one daemon, local
15// or remote, and the only one: no daemon monitors another. It keeps
16// configured authority separate from observed state:
17//
18// - configured authority: Local flag, Endpoint, Registration, Policy, and
19// the presentation hints DisplayOrigin and Rank. The registry stamps
20// them from its own host records (or the local composition) and never
21// trusts them from a probe.
22// - observed identity: Identity and Incarnation are zero until a probe
23// actually authenticated the daemon. Unknown identity is never invented.
24// - observed capability: ProtocolVersion and Capabilities mirror what the
25// daemon reported. ProtocolVersion is the daemon's own version and may
26// differ from Policy.ProtocolVersion; that difference is exactly how an
27// incompatible daemon is observable. Zero means unobserved.
28// - observation state: Availability, Checking, timestamps, failure
29// counters, and the exact session catalogue. Availability zero is
30// invalid: an unobserved daemon carries RemoteAvailabilityUnknown.
31//
32// Observing a daemon never attaches to it and never starts a stopped one.
33
34const (
35 // BrokerMaxDaemonsPerSnapshot bounds one snapshot publication: the local
36 // daemon plus every configured remote host.
37 BrokerMaxDaemonsPerSnapshot = BrokerMaxHosts + 1
38 // BrokerMaxDisplayOriginBytes bounds the presentation origin hint.
39 BrokerMaxDisplayOriginBytes = 128
40)
41
42// BrokerDaemonObservation is one immutable daemon projection in one broker
43// snapshot. Nested slices are never mutated after publication.
44type BrokerDaemonObservation struct {
45 // Local marks the broker's own machine daemon. A local observation has
46 // no endpoint and no registration: the local daemon is not a configured
47 // host. At most one local observation exists per snapshot.
48 Local bool
49
50 // Configured authority (registry-stamped, never probe-supplied).
51 Endpoint string // remote only; matches Registration.Endpoint
52 DisplayOrigin string
53 Rank int
54 Registration domain.RemoteRegistration
55 Policy BrokerPolicy
56
57 // Observed identity/incarnation (zero until observed).
58 Identity BrokerDaemonIdentity
59 Incarnation BrokerDaemonIncarnation
60
61 // Observed version/capability (zero until observed).
62 ProtocolVersion uint16
63 Capabilities uint32
64
65 // Availability, checking, failure, and freshness state.
66 Availability domain.RemoteAvailability
67 Checking bool
68 LastAttempt time.Time
69 LastSuccess time.Time
70 NextDue time.Time
71 ConsecutiveFailures uint
72 FailureEpisode uint64
73 LastFailure domain.RemoteFailure
74 InventoryKnown bool
75
76 // Exact session catalogue observed from the daemon.
77 Sessions []catalogue.RemoteCatalogSession
78}
79
80// Validate enforces the closed projection rules.
81func (o BrokerDaemonObservation) Validate() error {
82 // Policy is required authority for every daemon, observed or not.
83 if err := o.Policy.Validate(); err != nil {
84 return fmt.Errorf("ports: broker daemon observation: %w", err)
85 }
86 if err := validateBrokerDisplayText(o.DisplayOrigin, BrokerMaxDisplayOriginBytes, "display origin"); err != nil {
87 return fmt.Errorf("ports: broker daemon observation: %w", err)
88 }
89 if o.DisplayOrigin == "" {
90 return errors.New("ports: broker daemon observation has no display origin")
91 }
92 if o.Local {
93 if o.Endpoint != "" || o.Registration != (domain.RemoteRegistration{}) {
94 return errors.New("ports: local daemon observation carries remote authority")
95 }
96 } else {
97 if err := domain.ValidateRemoteHostTarget(o.Endpoint); err != nil {
98 return fmt.Errorf("ports: broker daemon observation: %w", err)
99 }
100 if err := o.Registration.Validate(); err != nil {
101 return fmt.Errorf("ports: broker daemon observation: %w", err)
102 }
103 if o.Endpoint != o.Registration.Endpoint {
104 return errors.New("ports: broker daemon observation endpoint does not match registration")
105 }
106 }
107 if err := validateObservedDaemonState(o); err != nil {
108 return err
109 }
110 if o.Availability < domain.RemoteAvailabilityUnknown || o.Availability > domain.RemoteAvailabilityNoDaemon {
111 return errors.New("ports: broker daemon observation availability is out of range")
112 }
113 if o.LastFailure.Kind > domain.RemoteFailureInvalidResponse {
114 return errors.New("ports: broker daemon observation failure kind is out of range")
115 }
116 if len(o.Sessions) > BrokerMaxSessionsPerHost {
117 return errors.New("ports: broker daemon observation has too many sessions")
118 }
119 if len(o.Sessions) > 0 && !o.InventoryKnown {
120 return errors.New("ports: broker daemon observation carries sessions without known inventory")
121 }
122 return nil
123}
124
125// validateObservedDaemonState applies the observed-state rules shared by a live
126// publication and a durable projection. Observed identity is all-or-nothing and
127// never invented: identity and incarnation appear together, both validate, and
128// an observed daemon always carries the protocol version its identity was
129// authenticated with. The offline store, a restored durable record, and the
130// registry all apply this one rule, so a projection either layer accepts is
131// never published as an observation the wire refuses.
132func validateObservedDaemonState(observation BrokerDaemonObservation) error {
133 observed := observation.Identity != ""
134 if observed != !observation.Incarnation.IsZero() {
135 return errors.New("ports: broker daemon observation has partial identity")
136 }
137 if !observed {
138 return nil
139 }
140 if err := observation.Identity.Validate(); err != nil {
141 return err
142 }
143 if err := observation.Incarnation.Validate(); err != nil {
144 return err
145 }
146 if observation.ProtocolVersion == 0 {
147 return errors.New("ports: observed daemon identity carries no protocol version")
148 }
149 return nil
150}
151
152// Clone returns a defensive copy with an independent session slice. Each
153// session keeps the exact nil-ness of its tab list: an empty but present list
154// is catalogue-valid and must not become absent through a copy.
155func (o BrokerDaemonObservation) Clone() BrokerDaemonObservation {
156 out := o
157 out.Sessions = append([]catalogue.RemoteCatalogSession(nil), o.Sessions...)
158 for i := range out.Sessions {
159 if o.Sessions[i].Tabs == nil {
160 continue
161 }
162 out.Sessions[i].Tabs = append(make([]catalogue.RemoteCatalogTab, 0, len(o.Sessions[i].Tabs)), o.Sessions[i].Tabs...)
163 }
164 return out
165}