-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathnara.go
More file actions
295 lines (258 loc) · 9.46 KB
/
Copy pathnara.go
File metadata and controls
295 lines (258 loc) · 9.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
package nara
import (
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"time"
"github.com/eljojo/nara/identity"
"github.com/eljojo/nara/types"
"github.com/shirou/gopsutil/v3/host"
"github.com/sirupsen/logrus"
)
const NaraVersion = "0.2.0"
type LocalNara struct {
Me *Nara
Network *Network
Soul string
ID types.NaraID // Nara ID: deterministic hash of soul+name for unique identity
Keypair identity.NaraKeypair
SyncLedger *SyncLedger // Unified event store for all syncable data (social + ping + future types)
Projections *ProjectionStore // Event-sourced projections for derived state
MemoryProfile MemoryProfile
forceChattiness int
isRaspberryPi bool
isNixOs bool
isKubernetes bool
mu sync.Mutex
}
// Notice there's also Nara struct in runtime/interfaces.go...
type Nara struct {
Name types.NaraName
Hostname string `json:"-"`
Version string
Status NaraStatus
ID types.NaraID // Nara ID from other naras (redundant with Status.ID but convenient)
mu sync.Mutex
// remember to sync with setValuesFrom
}
type NaraPersonality struct {
Agreeableness int // 0-100, high = more likely to join a clique
Sociability int // 0-100, high = more likely to start a clique
Chill int // 0-100, high = less likely to leave a clique
}
type NaraStatus struct {
LicensePlate string
Flair string
HostStats HostStats
Chattiness int64
Buzz int
Observations map[types.NaraName]NaraObservation
Trend string
TrendEmoji string
Personality NaraPersonality
Aura Aura // Visual identity (colors, glow, etc.) derived from personality and soul
Version string
PublicUrl string
PublicKey string // Base64-encoded Ed25519 public key
ID types.NaraID // Nara ID: deterministic hash of soul+name
MeshEnabled bool // True if this nara is connected to the Headscale mesh
MeshIP string // Tailscale IP for direct mesh communication (no DNS needed)
Coordinates *NetworkCoordinate `json:"coordinates,omitempty"` // Vivaldi network coordinates
TransportMode string `json:"transport_mode,omitempty"` // "mqtt", "gossip", or "hybrid"
EventStoreTotal int `json:"event_store_total,omitempty"`
EventStoreByService map[string]int `json:"event_store_by_service,omitempty"`
EventStoreCritical int `json:"event_store_critical,omitempty"`
MemoryMode string `json:"memory_mode,omitempty"`
MemoryBudgetMB int `json:"memory_budget_mb,omitempty"`
MemoryMaxEvents int `json:"memory_max_events,omitempty"`
StashStored int `json:"stash_stored,omitempty"` // Number of stashes stored for others
StashBytes int64 `json:"stash_bytes,omitempty"` // Total bytes of stash data stored
StashConfidants int `json:"stash_confidants,omitempty"` // Number of confidants storing my stash
// remember to sync with setValuesFrom
// NOTE: Soul was removed - NEVER serialize private keys!
}
func NewLocalNara(identityResult identity.IdentityResult, mqtt_host string, mqtt_user string, mqtt_pass string, forceChattiness int, memoryProfile MemoryProfile) (*LocalNara, error) {
logrus.Printf("📟 Booting nara: %s (%s)", identityResult.Name, identityResult.ID)
soulStr := identity.FormatSoul(identityResult.Soul)
if memoryProfile.MaxEvents <= 0 {
memoryProfile = DefaultMemoryProfile()
}
ln := &LocalNara{
Me: NewNara(identityResult.Name),
Soul: soulStr,
ID: identityResult.ID,
MemoryProfile: memoryProfile,
forceChattiness: forceChattiness,
isRaspberryPi: isRaspberryPi(),
isNixOs: isNixOs(),
isKubernetes: isKubernetes(),
}
ln.Me.Version = NaraVersion
ln.Me.Status.Version = NaraVersion
ln.Me.Status.Coordinates = NewNetworkCoordinate() // Initialize Vivaldi coordinates
ln.Me.Status.ID = identityResult.ID
ln.Me.Status.MemoryMode = string(memoryProfile.Mode)
ln.Me.Status.MemoryBudgetMB = memoryProfile.BudgetMB
ln.Me.Status.MemoryMaxEvents = memoryProfile.MaxEvents
// NOTE: Soul is NEVER set in Status - private keys must not be serialized!
// Derive Ed25519 keypair from soul
ln.Keypair = identity.DeriveKeypair(identityResult.Soul)
ln.Me.Status.PublicKey = identity.FormatPublicKey(ln.Keypair.PublicKey)
logrus.Printf("🔑 Keypair derived from soul")
ln.seedPersonality()
// Set aura after personality is initialized
ln.Me.Status.Aura = ln.computeAura()
// Initialize unified sync ledger for all service types (social + ping + observation)
// GUARANTEE: SyncLedger is ALWAYS non-nil after NewLocalNara() completes
// NOTE: Must be initialized BEFORE Network so CheckpointService can use it
ln.SyncLedger = NewSyncLedger(memoryProfile.MaxEvents)
if ln.SyncLedger == nil {
panic("SyncLedger initialization failed - this should never happen")
}
// Initialize projections after SyncLedger
ln.Projections = NewProjectionStore(ln.SyncLedger)
// Initialize network (needs SyncLedger for CheckpointService)
ln.Network = NewNetwork(ln, mqtt_host, mqtt_user, mqtt_pass)
ln.updateHostStats()
hostinfo, err := host.Info()
if err != nil || hostinfo == nil {
logrus.Warnf("⚠️ Warning: failed to get host info: %v", err)
} else {
ln.Me.Hostname = hostinfo.Hostname
}
observation := ln.getMeObservation()
observation.LastRestart = time.Now().Unix()
ln.setMeObservation(observation)
return ln, nil
}
func NewNara(name types.NaraName) *Nara {
nara := &Nara{Name: name}
nara.Status.Observations = make(map[types.NaraName]NaraObservation)
return nara
}
func (ln *LocalNara) Start(serveUI bool, readOnly bool, httpAddr string, meshConfig *TsnetConfig, transportMode TransportMode) {
ln.Network.ReadOnly = readOnly
ln.Network.TransportMode = transportMode
ln.Me.Status.TransportMode = transportMode.String() // Share our transport mode with peers
if serveUI {
logrus.Printf("💻 Serving UI for %s", ln.Me.Name)
}
// Start projections
if ln.Projections != nil {
// Configure MISSING threshold to account for gossip mode
ln.Projections.OnlineStatus().SetMissingThresholdFunc(func(name types.NaraName) int64 {
threshold := MissingThresholdSeconds
nara := ln.Network.getNara(name)
// Defensive: nara might not be found or might have been removed
subjectIsGossip := nara != nil && nara.Name != "" && nara.Status.TransportMode == "gossip"
observerIsGossip := ln.Network.TransportMode == TransportGossip
if subjectIsGossip || observerIsGossip {
threshold = MissingThresholdGossipSeconds
}
return threshold * int64(time.Second) // Convert to nanoseconds
})
ln.Projections.Start()
}
go ln.updateHostStatsForever()
ln.Network.Start(serveUI, httpAddr, meshConfig)
if readOnly {
logrus.Printf("🤫 Read-only mode: not pinging or announcing")
}
}
func (ln *LocalNara) SetupCloseHandler() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
fmt.Println("babaayyy")
ln.Network.Chau()
// Gracefully shutdown projections
if ln.Projections != nil {
ln.Projections.Shutdown()
}
// Gracefully shutdown all background goroutines
ln.Network.Shutdown()
ln.Network.disconnectMQTT()
time.Sleep(50 * time.Millisecond) // sleep a bit to ensure message is sent
os.Exit(0)
}()
}
func (ln *LocalNara) chattinessRate(min int64, max int64) int64 {
return min + ((max - min) * (100 - ln.Me.Status.Chattiness) / 100)
}
func (ln *LocalNara) uptime() int64 {
me := ln.getMeObservation()
return me.LastSeen - me.LastRestart
}
func (ln *LocalNara) isBooting() bool {
return ln.uptime() < 120
}
func (nara *Nara) setValuesFrom(other *Nara) {
nara.mu.Lock() // this protects Status
defer nara.mu.Unlock()
if other.Name == "" || other.Name != nara.Name {
logrus.Printf("warning: fed incorrect Nara to setValuesFrom: %s (expected %s)", other.Name, nara.Name)
return
}
if other.Version != "" {
nara.Version = other.Version
}
nara.Status.setValuesFrom(other.Status)
}
func (ns *NaraStatus) setValuesFrom(other NaraStatus) {
ns.LicensePlate = other.LicensePlate
if other.Flair != "" {
ns.Flair = other.Flair
}
if (other.HostStats != HostStats{}) {
ns.HostStats = other.HostStats
}
if other.Version != "" {
ns.Version = other.Version
}
ns.Trend = other.Trend
ns.TrendEmoji = other.TrendEmoji
ns.Personality = other.Personality
if other.Aura.Primary != "" {
ns.Aura = other.Aura
}
// NOTE: Soul is never copied - private keys must not be shared!
if other.LicensePlate != "" {
ns.LicensePlate = other.LicensePlate
}
ns.Chattiness = other.Chattiness
ns.Buzz = other.Buzz
if other.Observations != nil {
for name, nara := range other.Observations {
ns.Observations[name] = nara
}
}
if other.PublicUrl != "" {
ns.PublicUrl = other.PublicUrl
}
if other.PublicKey != "" {
ns.PublicKey = other.PublicKey
}
if other.ID != "" {
ns.ID = other.ID
}
if other.MemoryMode != "" {
ns.MemoryMode = other.MemoryMode
}
if other.MemoryBudgetMB != 0 {
ns.MemoryBudgetMB = other.MemoryBudgetMB
}
if other.MemoryMaxEvents != 0 {
ns.MemoryMaxEvents = other.MemoryMaxEvents
}
ns.MeshEnabled = other.MeshEnabled
ns.MeshIP = other.MeshIP
if other.Coordinates != nil {
ns.Coordinates = other.Coordinates
}
if other.TransportMode != "" {
ns.TransportMode = other.TransportMode
}
}