// forge-account-provider: gitea as the provider of `forge-account` (novox/hq ADR 0399, ADR 0400; issue 955). A process // the node's runtime launches and speaks MCP to over stdio, beside the module's other bundles. // // For each machine whose ssh-client binds the provision and has reported its public key (ssh-client's state `keys`), // it makes the machine's agent a forge account of its own (agent-, in the agents team), registers the key for // it, and serves where and as whom to reach the forge over ssh: the user, the forge machine's mesh name and port, its // host key and the two url roots. A public key is no secret, so the provision has no credential. // // stdout is the MCP channel; it says what it says on stderr, and never the admin's password. package main import ( "context" "fmt" "os" "os/exec" "strconv" "strings" "sync" "time" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/provider" "gitea/internal/forgehttp" ) func say(format string, args ...any) { fmt.Fprintf(os.Stderr, "[forge-account] "+format+"\n", args...) } // execRunner runs a program on this machine. type execRunner struct{} func (execRunner) Run(ctx context.Context, name string, args ...string) ([]byte, error) { ctx, cancel := context.WithTimeout(ctx, 20*time.Second) defer cancel() out, err := exec.CommandContext(ctx, name, args...).Output() if err != nil { return nil, fmt.Errorf("%s: %w", name, err) } return out, nil } // stateKeys is ssh-client's state `keys` over the runtime (the manifest's `reads` grants it). type stateKeys struct{} func (stateKeys) Watch(handle func(key, op string, value []byte, current bool)) error { return stdio.State("ssh-client.keys").Watch("", func(c stdio.StateChange) error { handle(c.Key, c.Op, c.Value, c.Current) return nil }) } // configFromEnv is the service's environment as the manifest hands it, naming what is missing. func configFromEnv(getenv func(string) string) (Config, error) { port, perr := strconv.Atoi(getenv("MESH_GITEA_SSH_PORT")) cfg := Config{ Node: getenv("MESH_NODE"), HostFile: getenv("MESH_FORGE_HOST_FILE"), OriginsFile: getenv("MESH_GITEA_ORIGINS_FILE"), Port: port, Forge: forgehttp.Client{URL: strings.TrimRight(getenv("MESH_GITEA_URL"), "/"), User: getenv("MESH_GITEA_ADMIN_USER"), PasswordFile: getenv("MESH_GITEA_ADMIN_PASSWORD_FILE"), Timeout: 20 * time.Second}, Runner: execRunner{}, } var missing []string for _, kv := range [][2]string{{"MESH_NODE", cfg.Node}, {"MESH_FORGE_HOST_FILE", cfg.HostFile}, {"MESH_GITEA_ORIGINS_FILE", cfg.OriginsFile}, {"MESH_GITEA_URL", cfg.Forge.URL}, {"MESH_GITEA_ADMIN_USER", cfg.Forge.User}, {"MESH_GITEA_ADMIN_PASSWORD_FILE", cfg.Forge.PasswordFile}} { if kv[1] == "" { missing = append(missing, kv[0]) } } if perr != nil || port <= 0 { missing = append(missing, "MESH_GITEA_SSH_PORT") } if len(missing) > 0 { return Config{}, fmt.Errorf("the forge-account provider cannot start: %s not set by the manifest", strings.Join(missing, ", ")) } return cfg, nil } func main() { cfg, err := configFromEnv(os.Getenv) if err != nil { // Said, not fatal: the runtime would restart a process that cannot do better. say("%v; providing nothing", err) if err := stdio.Serve("", nil); err != nil { say("%v", err) os.Exit(1) } return } cfg.Log = say svc := NewService(cfg) p := svc.Provider(provider.StateBus(Module)) ctx := context.Background() // A pass answers every consumer; one at a time, and one per change of a machine's key. var passing sync.Mutex pass := func() { passing.Lock() defer passing.Unlock() if _, err := p.Pass(ctx); err != nil { say("a pass failed: %v", err) } } svc.OnKeys = func(string) { go pass() } go func() { if err := svc.WatchKeys(ctx, stateKeys{}); err != nil { say("the machines' keys are not watched: %v", err) return } say("watching the machines' keys") pass() }() go func() { svc.KeepRunning(ctx, p.Run) }() if err := stdio.Serve("", nil); err != nil { say("%v", err) os.Exit(1) } }