ScuttleBot

scuttlebot / internal / registry / registry.go
Source Blame History 528 lines
1efddcb… lmata 1 // Package registry manages agent registration and credential lifecycle.
1efddcb… lmata 2 //
1efddcb… lmata 3 // Agents register with scuttlebot and receive SASL credentials for the Ergo
1efddcb… lmata 4 // IRC server, plus a signed rules-of-engagement payload describing their
1efddcb… lmata 5 // channel assignments and permissions.
cadb504… lmata 6 package registry
1efddcb… lmata 7
1efddcb… lmata 8 import (
1efddcb… lmata 9 "crypto/hmac"
1efddcb… lmata 10 "crypto/rand"
1efddcb… lmata 11 "crypto/sha256"
1efddcb… lmata 12 "encoding/hex"
1efddcb… lmata 13 "encoding/json"
1efddcb… lmata 14 "fmt"
5ac549c… lmata 15 "os"
5ac549c… lmata 16 "strings"
1efddcb… lmata 17 "sync"
1efddcb… lmata 18 "time"
0e78954… lmata 19
0e78954… lmata 20 "github.com/conflicthq/scuttlebot/internal/store"
1efddcb… lmata 21 )
1efddcb… lmata 22
1efddcb… lmata 23 // AgentType describes an agent's role and authority level.
1efddcb… lmata 24 type AgentType string
1efddcb… lmata 25
1efddcb… lmata 26 const (
98cb896… lmata 27 AgentTypeOperator AgentType = "operator" // human operator — +o + full permissions
1efddcb… lmata 28 AgentTypeOrchestrator AgentType = "orchestrator" // +o in channels
f7eb47b… lmata 29 AgentTypeWorker AgentType = "worker" // +v in channels
f7eb47b… lmata 30 AgentTypeObserver AgentType = "observer" // no special mode
1efddcb… lmata 31 )
1efddcb… lmata 32
1efddcb… lmata 33 // Agent is a registered agent.
1efddcb… lmata 34 type Agent struct {
7830697… lmata 35 Nick string `json:"nick"`
7830697… lmata 36 Type AgentType `json:"type"`
7830697… lmata 37 Channels []string `json:"channels"` // convenience: same as Config.Channels
7830697… lmata 38 Permissions []string `json:"permissions"` // convenience: same as Config.Permissions
7830697… lmata 39 Config EngagementConfig `json:"config"`
ba75f34… noreply 40 Skills []string `json:"skills,omitempty"` // agent capabilities (e.g. "go", "python", "react")
7830697… lmata 41 CreatedAt time.Time `json:"created_at"`
7830697… lmata 42 Revoked bool `json:"revoked"`
1cbc747… lmata 43 LastSeen *time.Time `json:"last_seen,omitempty"`
1cbc747… lmata 44 Online bool `json:"online"`
1efddcb… lmata 45 }
1efddcb… lmata 46
1efddcb… lmata 47 // Credentials are the SASL credentials an agent uses to connect to Ergo.
1efddcb… lmata 48 type Credentials struct {
1efddcb… lmata 49 Nick string `json:"nick"`
1efddcb… lmata 50 Passphrase string `json:"passphrase"`
1efddcb… lmata 51 }
1efddcb… lmata 52
1efddcb… lmata 53 // EngagementPayload is the signed payload delivered to an agent on registration.
7830697… lmata 54 // Agents verify this with VerifyPayload() before trusting its contents.
1efddcb… lmata 55 type EngagementPayload struct {
7830697… lmata 56 V int `json:"v"`
7830697… lmata 57 Nick string `json:"nick"`
7830697… lmata 58 Type AgentType `json:"type"`
7830697… lmata 59 Config EngagementConfig `json:"config"`
7830697… lmata 60 IssuedAt time.Time `json:"issued_at"`
1efddcb… lmata 61 }
1efddcb… lmata 62
1efddcb… lmata 63 // SignedPayload wraps an EngagementPayload with an HMAC signature.
1efddcb… lmata 64 type SignedPayload struct {
1efddcb… lmata 65 Payload EngagementPayload `json:"payload"`
1efddcb… lmata 66 Signature string `json:"signature"` // hex-encoded HMAC-SHA256
1efddcb… lmata 67 }
1efddcb… lmata 68
1efddcb… lmata 69 // AccountProvisioner is the interface the registry uses to create/modify IRC accounts.
1efddcb… lmata 70 // Implemented by *ergo.APIClient in production; can be mocked in tests.
1efddcb… lmata 71 type AccountProvisioner interface {
1efddcb… lmata 72 RegisterAccount(name, passphrase string) error
1efddcb… lmata 73 ChangePassword(name, passphrase string) error
1efddcb… lmata 74 }
1efddcb… lmata 75
1efddcb… lmata 76 // Registry manages registered agents and their credentials.
1efddcb… lmata 77 type Registry struct {
d924aea… lmata 78 mu sync.RWMutex
d924aea… lmata 79 agents map[string]*Agent // keyed by nick
d924aea… lmata 80 provisioner AccountProvisioner
d924aea… lmata 81 signingKey []byte
d924aea… lmata 82 dataPath string // path to persist agents JSON; empty = no persistence
d924aea… lmata 83 db *store.Store // when non-nil, supersedes dataPath
d924aea… lmata 84 onlineTimeout time.Duration
1efddcb… lmata 85 }
1efddcb… lmata 86
1efddcb… lmata 87 // New creates a new Registry with the given provisioner and HMAC signing key.
5ac549c… lmata 88 // Call SetDataPath to enable persistence before registering any agents.
1efddcb… lmata 89 func New(provisioner AccountProvisioner, signingKey []byte) *Registry {
1efddcb… lmata 90 return &Registry{
1efddcb… lmata 91 agents: make(map[string]*Agent),
1efddcb… lmata 92 provisioner: provisioner,
1efddcb… lmata 93 signingKey: signingKey,
1efddcb… lmata 94 }
5ac549c… lmata 95 }
5ac549c… lmata 96
0e78954… lmata 97 // SetDataPath enables file-based persistence. The registry is loaded from path
0e78954… lmata 98 // immediately (non-fatal if the file doesn't exist yet) and saved there after
0e78954… lmata 99 // every mutation. Mutually exclusive with SetStore.
5ac549c… lmata 100 func (r *Registry) SetDataPath(path string) error {
5ac549c… lmata 101 r.mu.Lock()
5ac549c… lmata 102 defer r.mu.Unlock()
5ac549c… lmata 103 r.dataPath = path
5ac549c… lmata 104 return r.load()
0e78954… lmata 105 }
0e78954… lmata 106
0e78954… lmata 107 // SetStore switches the registry to database-backed persistence. All current
0e78954… lmata 108 // in-memory state is replaced with rows loaded from the store. Mutually
0e78954… lmata 109 // exclusive with SetDataPath.
0e78954… lmata 110 func (r *Registry) SetStore(db *store.Store) error {
0e78954… lmata 111 rows, err := db.AgentList()
0e78954… lmata 112 if err != nil {
0e78954… lmata 113 return fmt.Errorf("registry: load from store: %w", err)
0e78954… lmata 114 }
0e78954… lmata 115 r.mu.Lock()
0e78954… lmata 116 defer r.mu.Unlock()
0e78954… lmata 117 r.db = db
0e78954… lmata 118 r.dataPath = "" // DB takes over
0e78954… lmata 119 r.agents = make(map[string]*Agent, len(rows))
0e78954… lmata 120 for _, row := range rows {
0e78954… lmata 121 var cfg EngagementConfig
0e78954… lmata 122 if err := json.Unmarshal(row.Config, &cfg); err != nil {
0e78954… lmata 123 return fmt.Errorf("registry: decode agent %s config: %w", row.Nick, err)
0e78954… lmata 124 }
0e78954… lmata 125 a := &Agent{
0e78954… lmata 126 Nick: row.Nick,
0e78954… lmata 127 Type: AgentType(row.Type),
0e78954… lmata 128 Channels: cfg.Channels,
0e78954… lmata 129 Permissions: cfg.Permissions,
0e78954… lmata 130 Config: cfg,
0e78954… lmata 131 CreatedAt: row.CreatedAt,
0e78954… lmata 132 Revoked: row.Revoked,
66d18d7… lmata 133 LastSeen: row.LastSeen,
0e78954… lmata 134 }
0e78954… lmata 135 r.agents[a.Nick] = a
0e78954… lmata 136 }
0e78954… lmata 137 return nil
0e78954… lmata 138 }
0e78954… lmata 139
0e78954… lmata 140 // saveOne persists a single agent. Uses the DB when available, otherwise
0e78954… lmata 141 // falls back to a full file rewrite.
0e78954… lmata 142 func (r *Registry) saveOne(a *Agent) {
0e78954… lmata 143 if r.db != nil {
0e78954… lmata 144 cfg, _ := json.Marshal(a.Config)
0e78954… lmata 145 _ = r.db.AgentUpsert(&store.AgentRow{
0e78954… lmata 146 Nick: a.Nick,
0e78954… lmata 147 Type: string(a.Type),
0e78954… lmata 148 Config: cfg,
0e78954… lmata 149 CreatedAt: a.CreatedAt,
0e78954… lmata 150 Revoked: a.Revoked,
66d18d7… lmata 151 LastSeen: a.LastSeen,
0e78954… lmata 152 })
0e78954… lmata 153 return
0e78954… lmata 154 }
0e78954… lmata 155 r.save()
0e78954… lmata 156 }
0e78954… lmata 157
0e78954… lmata 158 // deleteOne removes a single agent from the store. Uses the DB when available,
0e78954… lmata 159 // otherwise falls back to a full file rewrite (agent already removed from map).
0e78954… lmata 160 func (r *Registry) deleteOne(nick string) {
0e78954… lmata 161 if r.db != nil {
0e78954… lmata 162 _ = r.db.AgentDelete(nick)
0e78954… lmata 163 return
0e78954… lmata 164 }
0e78954… lmata 165 r.save()
5ac549c… lmata 166 }
5ac549c… lmata 167
5ac549c… lmata 168 func (r *Registry) load() error {
5ac549c… lmata 169 data, err := os.ReadFile(r.dataPath)
5ac549c… lmata 170 if os.IsNotExist(err) {
5ac549c… lmata 171 return nil
5ac549c… lmata 172 }
5ac549c… lmata 173 if err != nil {
5ac549c… lmata 174 return fmt.Errorf("registry: load: %w", err)
5ac549c… lmata 175 }
5ac549c… lmata 176 var agents []*Agent
5ac549c… lmata 177 if err := json.Unmarshal(data, &agents); err != nil {
5ac549c… lmata 178 return fmt.Errorf("registry: load: %w", err)
5ac549c… lmata 179 }
5ac549c… lmata 180 for _, a := range agents {
5ac549c… lmata 181 r.agents[a.Nick] = a
5ac549c… lmata 182 }
5ac549c… lmata 183 return nil
5ac549c… lmata 184 }
5ac549c… lmata 185
5ac549c… lmata 186 func (r *Registry) save() {
5ac549c… lmata 187 if r.dataPath == "" {
5ac549c… lmata 188 return
5ac549c… lmata 189 }
5ac549c… lmata 190 agents := make([]*Agent, 0, len(r.agents))
5ac549c… lmata 191 for _, a := range r.agents {
5ac549c… lmata 192 agents = append(agents, a)
5ac549c… lmata 193 }
5ac549c… lmata 194 data, err := json.MarshalIndent(agents, "", " ")
5ac549c… lmata 195 if err != nil {
5ac549c… lmata 196 return
5ac549c… lmata 197 }
5ac549c… lmata 198 _ = os.WriteFile(r.dataPath, data, 0600)
7830697… lmata 199 }
7830697… lmata 200
1efddcb… lmata 201 // Register creates a new agent, provisions its Ergo account, and returns
1efddcb… lmata 202 // credentials and a signed rules-of-engagement payload.
7830697… lmata 203 // cfg is validated before any provisioning occurs.
7830697… lmata 204 func (r *Registry) Register(nick string, agentType AgentType, cfg EngagementConfig) (*Credentials, *SignedPayload, error) {
7830697… lmata 205 if err := cfg.Validate(); err != nil {
7830697… lmata 206 return nil, nil, fmt.Errorf("registry: invalid engagement config: %w", err)
7830697… lmata 207 }
7830697… lmata 208
1efddcb… lmata 209 r.mu.Lock()
1efddcb… lmata 210 defer r.mu.Unlock()
1efddcb… lmata 211
1efddcb… lmata 212 if existing, ok := r.agents[nick]; ok && !existing.Revoked {
1efddcb… lmata 213 return nil, nil, fmt.Errorf("registry: agent %q already registered", nick)
1efddcb… lmata 214 }
1efddcb… lmata 215
1efddcb… lmata 216 passphrase, err := generatePassphrase()
1efddcb… lmata 217 if err != nil {
1efddcb… lmata 218 return nil, nil, fmt.Errorf("registry: generate passphrase: %w", err)
1efddcb… lmata 219 }
1efddcb… lmata 220
1efddcb… lmata 221 if err := r.provisioner.RegisterAccount(nick, passphrase); err != nil {
5ac549c… lmata 222 // Account exists in NickServ from a previous run — sync the password.
5ac549c… lmata 223 if strings.Contains(err.Error(), "ACCOUNT_EXISTS") {
5ac549c… lmata 224 if err2 := r.provisioner.ChangePassword(nick, passphrase); err2 != nil {
5ac549c… lmata 225 return nil, nil, fmt.Errorf("registry: provision account: %w", err2)
5ac549c… lmata 226 }
5ac549c… lmata 227 } else {
5ac549c… lmata 228 return nil, nil, fmt.Errorf("registry: provision account: %w", err)
5ac549c… lmata 229 }
5ac549c… lmata 230 }
5ac549c… lmata 231
5ac549c… lmata 232 agent := &Agent{
5ac549c… lmata 233 Nick: nick,
5ac549c… lmata 234 Type: agentType,
5ac549c… lmata 235 Channels: cfg.Channels,
5ac549c… lmata 236 Permissions: cfg.Permissions,
5ac549c… lmata 237 Config: cfg,
5ac549c… lmata 238 CreatedAt: time.Now(),
5ac549c… lmata 239 }
5ac549c… lmata 240 r.agents[nick] = agent
0e78954… lmata 241 r.saveOne(agent)
5ac549c… lmata 242
5ac549c… lmata 243 payload, err := r.signPayload(agent)
5ac549c… lmata 244 if err != nil {
5ac549c… lmata 245 return nil, nil, fmt.Errorf("registry: sign payload: %w", err)
5ac549c… lmata 246 }
5ac549c… lmata 247
5ac549c… lmata 248 return &Credentials{Nick: nick, Passphrase: passphrase}, payload, nil
5ac549c… lmata 249 }
5ac549c… lmata 250
5ac549c… lmata 251 // Adopt adds a pre-existing NickServ account to the registry without touching
5ac549c… lmata 252 // its password. The caller is responsible for knowing their own passphrase.
5ac549c… lmata 253 // Returns a signed payload; no Credentials are returned since the password
5ac549c… lmata 254 // is not changed.
5ac549c… lmata 255 func (r *Registry) Adopt(nick string, agentType AgentType, cfg EngagementConfig) (*SignedPayload, error) {
5ac549c… lmata 256 if err := cfg.Validate(); err != nil {
5ac549c… lmata 257 return nil, fmt.Errorf("registry: invalid engagement config: %w", err)
5ac549c… lmata 258 }
5ac549c… lmata 259
5ac549c… lmata 260 r.mu.Lock()
5ac549c… lmata 261 defer r.mu.Unlock()
5ac549c… lmata 262
5ac549c… lmata 263 if existing, ok := r.agents[nick]; ok && !existing.Revoked {
5ac549c… lmata 264 return nil, fmt.Errorf("registry: agent %q already registered", nick)
7830697… lmata 265 }
7830697… lmata 266
7830697… lmata 267 agent := &Agent{
7830697… lmata 268 Nick: nick,
7830697… lmata 269 Type: agentType,
7830697… lmata 270 Channels: cfg.Channels,
7830697… lmata 271 Permissions: cfg.Permissions,
7830697… lmata 272 Config: cfg,
7830697… lmata 273 CreatedAt: time.Now(),
7830697… lmata 274 }
7830697… lmata 275 r.agents[nick] = agent
0e78954… lmata 276 r.saveOne(agent)
7830697… lmata 277
5ac549c… lmata 278 return r.signPayload(agent)
7830697… lmata 279 }
7830697… lmata 280
1efddcb… lmata 281 // Rotate generates a new passphrase for an agent and updates Ergo.
1efddcb… lmata 282 func (r *Registry) Rotate(nick string) (*Credentials, error) {
1efddcb… lmata 283 r.mu.Lock()
1efddcb… lmata 284 defer r.mu.Unlock()
1efddcb… lmata 285
5ac549c… lmata 286 if _, err := r.get(nick); err != nil {
1efddcb… lmata 287 return nil, err
1efddcb… lmata 288 }
1efddcb… lmata 289
1efddcb… lmata 290 passphrase, err := generatePassphrase()
1efddcb… lmata 291 if err != nil {
1efddcb… lmata 292 return nil, fmt.Errorf("registry: generate passphrase: %w", err)
1efddcb… lmata 293 }
1efddcb… lmata 294
1efddcb… lmata 295 if err := r.provisioner.ChangePassword(nick, passphrase); err != nil {
1efddcb… lmata 296 return nil, fmt.Errorf("registry: rotate credentials: %w", err)
1efddcb… lmata 297 }
1efddcb… lmata 298
0e78954… lmata 299 // Rotation doesn't change stored agent data, but bump a file save for
0e78954… lmata 300 // consistency; DB backends are unaffected since nothing persisted changed.
5ac549c… lmata 301 r.save()
1efddcb… lmata 302 return &Credentials{Nick: nick, Passphrase: passphrase}, nil
1efddcb… lmata 303 }
1efddcb… lmata 304
1efddcb… lmata 305 // Revoke locks an agent out by rotating to an unguessable passphrase and
1efddcb… lmata 306 // marking it revoked in the registry.
1efddcb… lmata 307 func (r *Registry) Revoke(nick string) error {
1efddcb… lmata 308 r.mu.Lock()
1efddcb… lmata 309 defer r.mu.Unlock()
1efddcb… lmata 310
1efddcb… lmata 311 agent, err := r.get(nick)
1efddcb… lmata 312 if err != nil {
1efddcb… lmata 313 return err
1efddcb… lmata 314 }
1efddcb… lmata 315
1efddcb… lmata 316 lockout, err := generatePassphrase()
1efddcb… lmata 317 if err != nil {
1efddcb… lmata 318 return fmt.Errorf("registry: generate lockout passphrase: %w", err)
1efddcb… lmata 319 }
1efddcb… lmata 320
1efddcb… lmata 321 if err := r.provisioner.ChangePassword(nick, lockout); err != nil {
1efddcb… lmata 322 return fmt.Errorf("registry: revoke credentials: %w", err)
1efddcb… lmata 323 }
1efddcb… lmata 324
1efddcb… lmata 325 agent.Revoked = true
0e78954… lmata 326 r.saveOne(agent)
5ac549c… lmata 327 return nil
5ac549c… lmata 328 }
5ac549c… lmata 329
5ac549c… lmata 330 // Delete fully removes an agent from the registry. The Ergo NickServ account
5ac549c… lmata 331 // is locked out first (password rotated to an unguessable value) so the agent
5ac549c… lmata 332 // can no longer connect, then the entry is removed from the registry. If the
5ac549c… lmata 333 // agent is already revoked the lockout step is skipped.
5ac549c… lmata 334 func (r *Registry) Delete(nick string) error {
5ac549c… lmata 335 r.mu.Lock()
5ac549c… lmata 336 defer r.mu.Unlock()
5ac549c… lmata 337
5ac549c… lmata 338 agent, ok := r.agents[nick]
5ac549c… lmata 339 if !ok {
5ac549c… lmata 340 return fmt.Errorf("registry: agent %q not found", nick)
5ac549c… lmata 341 }
5ac549c… lmata 342
5ac549c… lmata 343 if !agent.Revoked {
5ac549c… lmata 344 lockout, err := generatePassphrase()
5ac549c… lmata 345 if err != nil {
5ac549c… lmata 346 return fmt.Errorf("registry: generate lockout passphrase: %w", err)
5ac549c… lmata 347 }
5ac549c… lmata 348 if err := r.provisioner.ChangePassword(nick, lockout); err != nil {
5ac549c… lmata 349 return fmt.Errorf("registry: delete lockout: %w", err)
5ac549c… lmata 350 }
5ac549c… lmata 351 }
5ac549c… lmata 352
5ac549c… lmata 353 delete(r.agents, nick)
0e78954… lmata 354 r.deleteOne(nick)
87e6978… lmata 355 return nil
87e6978… lmata 356 }
87e6978… lmata 357
87e6978… lmata 358 // UpdateChannels replaces the channel list for an active agent.
87e6978… lmata 359 // Used by relay brokers to sync runtime /join and /part changes back to the registry.
ba75f34… noreply 360 // Update persists changes to an existing agent record.
ba75f34… noreply 361 func (r *Registry) Update(agent *Agent) error {
ba75f34… noreply 362 r.mu.Lock()
ba75f34… noreply 363 defer r.mu.Unlock()
ba75f34… noreply 364 if _, ok := r.agents[agent.Nick]; !ok {
ba75f34… noreply 365 return fmt.Errorf("registry: agent %q not found", agent.Nick)
ba75f34… noreply 366 }
ba75f34… noreply 367 r.agents[agent.Nick] = agent
ba75f34… noreply 368 r.saveOne(agent)
ba75f34… noreply 369 return nil
ba75f34… noreply 370 }
ba75f34… noreply 371
87e6978… lmata 372 func (r *Registry) UpdateChannels(nick string, channels []string) error {
87e6978… lmata 373 r.mu.Lock()
87e6978… lmata 374 defer r.mu.Unlock()
87e6978… lmata 375 agent, err := r.get(nick)
87e6978… lmata 376 if err != nil {
87e6978… lmata 377 return err
87e6978… lmata 378 }
87e6978… lmata 379 agent.Channels = append([]string(nil), channels...)
87e6978… lmata 380 agent.Config.Channels = append([]string(nil), channels...)
0e78954… lmata 381 r.saveOne(agent)
1efddcb… lmata 382 return nil
1efddcb… lmata 383 }
1efddcb… lmata 384
1efddcb… lmata 385 // Get returns the agent with the given nick.
1efddcb… lmata 386 func (r *Registry) Get(nick string) (*Agent, error) {
1efddcb… lmata 387 r.mu.RLock()
1efddcb… lmata 388 defer r.mu.RUnlock()
1efddcb… lmata 389 return r.get(nick)
1efddcb… lmata 390 }
1efddcb… lmata 391
66d18d7… lmata 392 // Touch updates the last-seen timestamp for an agent. Persists to disk
66d18d7… lmata 393 // at most once per minute to avoid thrashing on frequent heartbeats.
1cbc747… lmata 394 func (r *Registry) Touch(nick string) {
1cbc747… lmata 395 r.mu.Lock()
1cbc747… lmata 396 defer r.mu.Unlock()
1cbc747… lmata 397 a, ok := r.agents[nick]
1cbc747… lmata 398 if !ok || a.Revoked {
1cbc747… lmata 399 return
1cbc747… lmata 400 }
1cbc747… lmata 401 now := time.Now()
66d18d7… lmata 402 shouldPersist := a.LastSeen == nil || now.Sub(*a.LastSeen) >= time.Minute
1cbc747… lmata 403 a.LastSeen = &now
66d18d7… lmata 404 if shouldPersist {
66d18d7… lmata 405 r.saveOne(a)
66d18d7… lmata 406 }
c68066e… lmata 407 }
c68066e… lmata 408
c68066e… lmata 409 const defaultOnlineTimeout = 2 * time.Minute
c68066e… lmata 410
c68066e… lmata 411 // SetOnlineTimeout configures how long since last_seen before an agent
c68066e… lmata 412 // is considered offline. Pass 0 to reset to the default (2 minutes).
c68066e… lmata 413 func (r *Registry) SetOnlineTimeout(d time.Duration) {
c68066e… lmata 414 r.mu.Lock()
c68066e… lmata 415 defer r.mu.Unlock()
c68066e… lmata 416 r.onlineTimeout = d
c68066e… lmata 417 }
c68066e… lmata 418
c68066e… lmata 419 func (r *Registry) getOnlineTimeout() time.Duration {
c68066e… lmata 420 if r.onlineTimeout > 0 {
c68066e… lmata 421 return r.onlineTimeout
c68066e… lmata 422 }
c68066e… lmata 423 return defaultOnlineTimeout
cd79584… lmata 424 }
cd79584… lmata 425
cd79584… lmata 426 // Reap removes agents that haven't been seen in maxAge. Revoked agents
cd79584… lmata 427 // are always reaped if older than maxAge. Returns the number of agents removed.
cd79584… lmata 428 func (r *Registry) Reap(maxAge time.Duration) int {
cd79584… lmata 429 if maxAge <= 0 {
cd79584… lmata 430 return 0
cd79584… lmata 431 }
cd79584… lmata 432 r.mu.Lock()
cd79584… lmata 433 defer r.mu.Unlock()
cd79584… lmata 434 cutoff := time.Now().Add(-maxAge)
cd79584… lmata 435 var reaped int
cd79584… lmata 436 for nick, a := range r.agents {
cd79584… lmata 437 if a.Online {
cd79584… lmata 438 continue
cd79584… lmata 439 }
cd79584… lmata 440 // Use last_seen if available, otherwise fall back to created_at.
cd79584… lmata 441 ref := a.CreatedAt
cd79584… lmata 442 if a.LastSeen != nil {
cd79584… lmata 443 ref = *a.LastSeen
cd79584… lmata 444 }
cd79584… lmata 445 if ref.Before(cutoff) {
cd79584… lmata 446 delete(r.agents, nick)
cd79584… lmata 447 if r.db != nil {
cd79584… lmata 448 _ = r.db.AgentDelete(nick)
cd79584… lmata 449 }
cd79584… lmata 450 reaped++
cd79584… lmata 451 }
cd79584… lmata 452 }
cd79584… lmata 453 if reaped > 0 && r.db == nil {
cd79584… lmata 454 r.save()
cd79584… lmata 455 }
cd79584… lmata 456 return reaped
c68066e… lmata 457 }
1cbc747… lmata 458
1cbc747… lmata 459 // List returns all registered agents with computed online status.
1efddcb… lmata 460 func (r *Registry) List() []*Agent {
1efddcb… lmata 461 r.mu.RLock()
1efddcb… lmata 462 defer r.mu.RUnlock()
c68066e… lmata 463 threshold := r.getOnlineTimeout()
1cbc747… lmata 464 now := time.Now()
1efddcb… lmata 465 var out []*Agent
1efddcb… lmata 466 for _, a := range r.agents {
c68066e… lmata 467 a.Online = a.LastSeen != nil && now.Sub(*a.LastSeen) < threshold
1cbc747… lmata 468 out = append(out, a)
1efddcb… lmata 469 }
1efddcb… lmata 470 return out
1efddcb… lmata 471 }
1efddcb… lmata 472
1efddcb… lmata 473 func (r *Registry) get(nick string) (*Agent, error) {
1efddcb… lmata 474 agent, ok := r.agents[nick]
1efddcb… lmata 475 if !ok {
1efddcb… lmata 476 return nil, fmt.Errorf("registry: agent %q not found", nick)
1efddcb… lmata 477 }
1efddcb… lmata 478 if agent.Revoked {
1efddcb… lmata 479 return nil, fmt.Errorf("registry: agent %q is revoked", nick)
1efddcb… lmata 480 }
1efddcb… lmata 481 return agent, nil
1efddcb… lmata 482 }
1efddcb… lmata 483
1efddcb… lmata 484 func (r *Registry) signPayload(agent *Agent) (*SignedPayload, error) {
1efddcb… lmata 485 payload := EngagementPayload{
7830697… lmata 486 V: 1,
7830697… lmata 487 Nick: agent.Nick,
7830697… lmata 488 Type: agent.Type,
7830697… lmata 489 Config: agent.Config,
7830697… lmata 490 IssuedAt: time.Now(),
1efddcb… lmata 491 }
1efddcb… lmata 492
1efddcb… lmata 493 data, err := json.Marshal(payload)
1efddcb… lmata 494 if err != nil {
1efddcb… lmata 495 return nil, err
1efddcb… lmata 496 }
1efddcb… lmata 497
1efddcb… lmata 498 mac := hmac.New(sha256.New, r.signingKey)
1efddcb… lmata 499 mac.Write(data)
1efddcb… lmata 500 sig := hex.EncodeToString(mac.Sum(nil))
1efddcb… lmata 501
1efddcb… lmata 502 return &SignedPayload{Payload: payload, Signature: sig}, nil
1efddcb… lmata 503 }
1efddcb… lmata 504
1efddcb… lmata 505 // VerifyPayload verifies the HMAC signature on a SignedPayload.
1efddcb… lmata 506 func VerifyPayload(sp *SignedPayload, signingKey []byte) error {
1efddcb… lmata 507 data, err := json.Marshal(sp.Payload)
1efddcb… lmata 508 if err != nil {
1efddcb… lmata 509 return err
1efddcb… lmata 510 }
1efddcb… lmata 511
1efddcb… lmata 512 mac := hmac.New(sha256.New, signingKey)
1efddcb… lmata 513 mac.Write(data)
1efddcb… lmata 514 expected := hex.EncodeToString(mac.Sum(nil))
1efddcb… lmata 515
1efddcb… lmata 516 if !hmac.Equal([]byte(sp.Signature), []byte(expected)) {
1efddcb… lmata 517 return fmt.Errorf("registry: invalid payload signature")
1efddcb… lmata 518 }
1efddcb… lmata 519 return nil
1efddcb… lmata 520 }
1efddcb… lmata 521
1efddcb… lmata 522 func generatePassphrase() (string, error) {
1efddcb… lmata 523 b := make([]byte, 32)
1efddcb… lmata 524 if _, err := rand.Read(b); err != nil {
1efddcb… lmata 525 return "", err
1efddcb… lmata 526 }
1efddcb… lmata 527 return hex.EncodeToString(b), nil
1efddcb… lmata 528 }

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button