package main // The key this machine's account uses to reach the forge over SSH (novox/hq ADR 0399, ADR 0400; issue 955, // area S1). It is made here, on the machine, once: ssh-keygen writes ~/.ssh/mesh_ed25519 only when it is // missing, so the private half never leaves the machine and an existing key is never replaced. The public // half and its fingerprint go into this module's `keys` state, one entry per account under ., // for the forge's provider to register. The tools bundle is the one process of this module, so the keeper // runs inside it beside the tools (main.go), the way claude-code reports what it holds. import ( "context" "fmt" "os" "path/filepath" "strings" "time" ) // KeyFile is the key's name under ~/.ssh: never one of the operator's own id_* files. const KeyFile = "mesh_ed25519" // KeyKind is the kind of key this process makes: the machine's account's own (ADR 0400 ยง1). const KeyKind = "machine" // KeyRetry is how soon it looks again after a failure. const KeyRetry = 30 * time.Second // KeyRound is how often the keeper looks again. const KeyRound = 10 * time.Minute // KeyEntry is one value of the keys state. It carries the public half only. type KeyEntry struct { Node string `json:"node"` Account string `json:"account"` Kind string `json:"kind"` Public string `json:"public"` Fingerprint string `json:"fingerprint"` At string `json:"at"` } // KeyPutter is the keys state, as far as the keeper needs it. type KeyPutter interface { Put(key string, value any) error } // KeyKeeper keeps one account's key and its entry. type KeyKeeper struct { Home, Node, Account string Run Runner State KeyPutter Now func() time.Time reported string // the public half and fingerprint last put } func (k *KeyKeeper) path() string { return filepath.Join(k.Home, ".ssh", KeyFile) } // Once makes the key when it is missing and puts the entry when it differs from the one last put. func (k *KeyKeeper) Once(ctx context.Context) error { if err := k.ensure(ctx); err != nil { return err } raw, err := os.ReadFile(k.path() + ".pub") if err != nil { return fmt.Errorf("the public half: %w", err) } pk, err := ParseKeyLine(string(raw)) if err != nil { return fmt.Errorf("%s.pub: %w", k.path(), err) } public := strings.TrimSpace(string(raw)) if sig := public + "|" + pk.Fingerprint; sig == k.reported { return nil } entry := KeyEntry{Node: k.Node, Account: k.Account, Kind: KeyKind, Public: public, Fingerprint: pk.Fingerprint, At: k.Now().UTC().Format(time.RFC3339)} if err := k.State.Put(k.Node+"."+k.Account, entry); err != nil { return err } k.reported = public + "|" + pk.Fingerprint return nil } // ensure makes the pair when the private half is missing. Never over an existing key. func (k *KeyKeeper) ensure(ctx context.Context) error { if _, err := os.Lstat(k.path()); err == nil { return nil } else if !os.IsNotExist(err) { return err } if err := os.MkdirAll(filepath.Dir(k.path()), 0o700); err != nil { return err } r := k.Run(ctx, nil, "ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", k.Account+"@"+k.Node+" (mesh)", "-f", k.path()) if r.Status != 0 || r.Err != "" { return fmt.Errorf("ssh-keygen: %s", firstLine(r.Err+" "+r.Stderr)) } return nil } // Keep runs Once now and every KeyRound, reporting a failure through say and trying again at the next round. func (k *KeyKeeper) Keep(ctx context.Context, say func(string, ...any)) { for { pause := KeyRound if err := k.Once(ctx); err != nil { say("the forge key: %v", err) pause = KeyRetry // the state or the bus's grant may arrive after the process } select { case <-ctx.Done(): return case <-time.After(pause): } } }