Files
agent-helm/helmd/main.go
Bjorn Blomberg 77682e91b5
All checks were successful
helmd-image / build (push) Successful in 3m22s
helmd-release / build-release (push) Successful in 4m28s
helmd hub: central server på Pi5 + agent-uplink + helmd ctl + web-login
Hub-läge i samma binär (doc/hub-plan.md): agenter ansluter utåt med
delad agent-nyckel (hub-sektion i config.json), web-/CLI-klienter
loggar in (lokala konton, pbkdf2; Authenticator-interface för LDAP
senare) och styr alla agenters sessioner via proxy + merged SSE.
Webbklienten autodetekterar hub/local via /api/mode. Nya kommandon:
helmd hub [setpass|users|agentkey], helmd ctl (login/agents/sessions/
prompt/screen/answer/events …). Enhetstester + scripts/run-hub-smoke.sh
(15-stegs e2e: hub + agent + ctl).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011KikHkfCiC3yELbsMN8fT9
2026-08-06 23:19:18 +02:00

232 lines
6.0 KiB
Go

package main
import (
"bufio"
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
var version = "dev" // sätts av CI via -ldflags
var exec_LookPath = exec.LookPath
func main() {
if len(os.Args) < 2 {
usage()
os.Exit(2)
}
switch os.Args[1] {
case "serve":
fs := flag.NewFlagSet("serve", flag.ExitOnError)
listen := fs.String("listen", "", "override listen-adress (t.ex. 0.0.0.0:8788)")
confPath := fs.String("config", configPath(), "sökväg till config.json")
fs.Parse(os.Args[2:])
cfg, err := loadConfig(*confPath)
if err != nil {
fatal(err)
}
if *listen != "" {
cfg.Listen = *listen
}
if err := serve(cfg); err != nil {
fatal(err)
}
case "token":
cfg, err := loadConfig(configPath())
if err != nil {
fatal(err)
}
fmt.Println(cfg.Token)
case "share":
fs := flag.NewFlagSet("share", flag.ExitOnError)
server := fs.String("server", "http://127.0.0.1:8788", "helmd-serverns adress")
note := fs.String("note", "", "beskrivning som visas i klienten")
session := fs.String("session", "", "session som delningen hör till")
fs.Parse(os.Args[2:])
if fs.NArg() != 1 {
fatal(fmt.Errorf("användning: helmd share [flaggor] <fil>"))
}
cfg, err := loadConfig(configPath())
if err != nil {
fatal(err)
}
id, err := shareFile(*server, cfg.Token, fs.Arg(0), *note, *session)
if err != nil {
fatal(err)
}
fmt.Printf("delad: %s (id %s)\n%s/api/shares/%s/raw\n", filepath.Base(fs.Arg(0)), id, *server, id)
case "hub":
if err := runHub(os.Args[2:]); err != nil {
fatal(err)
}
case "ctl":
if err := runCtl(os.Args[2:]); err != nil {
fatal(err)
}
case "version", "--version", "-v":
fmt.Println("helmd", version)
default:
usage()
os.Exit(2)
}
}
// runHub hanterar hub-subkommandona: starta hubben eller administrera
// dess konton/agent-nyckel.
func runHub(args []string) error {
if len(args) > 0 && !strings.HasPrefix(args[0], "-") {
switch args[0] {
case "setpass":
fs := flag.NewFlagSet("hub setpass", flag.ExitOnError)
confPath := fs.String("config", hubConfigPath(), "sökväg till hub.json")
password := fs.String("password", "", "lösenord (utelämna för att läsa från stdin)")
fs.Parse(args[1:])
if fs.NArg() != 1 {
return fmt.Errorf("användning: helmd hub setpass <användare> [--password PW]")
}
cfg, err := loadHubConfig(*confPath)
if err != nil {
return err
}
pw := *password
if pw == "" {
fmt.Fprintf(os.Stderr, "nytt lösenord för %s (syns när du skriver): ", fs.Arg(0))
sc := bufio.NewScanner(os.Stdin)
if sc.Scan() {
pw = strings.TrimSpace(sc.Text())
}
}
if err := cfg.SetPassword(fs.Arg(0), pw); err != nil {
return err
}
if err := cfg.Save(); err != nil {
return err
}
fmt.Printf("lösenord satt för %s — starta om hubben så att det börjar gälla\n", fs.Arg(0))
return nil
case "users":
fs := flag.NewFlagSet("hub users", flag.ExitOnError)
confPath := fs.String("config", hubConfigPath(), "sökväg till hub.json")
fs.Parse(args[1:])
cfg, err := loadHubConfig(*confPath)
if err != nil {
return err
}
for _, u := range cfg.Users {
fmt.Println(u.Username)
}
return nil
case "agentkey":
fs := flag.NewFlagSet("hub agentkey", flag.ExitOnError)
confPath := fs.String("config", hubConfigPath(), "sökväg till hub.json")
fs.Parse(args[1:])
cfg, err := loadHubConfig(*confPath)
if err != nil {
return err
}
fmt.Println(cfg.AgentKey)
return nil
default:
return fmt.Errorf("okänt hub-kommando %q (setpass, users, agentkey — eller flaggor för att starta)", args[0])
}
}
fs := flag.NewFlagSet("hub", flag.ExitOnError)
listen := fs.String("listen", "", "override listen-adress (t.ex. 0.0.0.0:8790)")
confPath := fs.String("config", hubConfigPath(), "sökväg till hub.json")
fs.Parse(args)
cfg, err := loadHubConfig(*confPath)
if err != nil {
return err
}
if *listen != "" {
cfg.Listen = *listen
}
return hubServe(cfg)
}
func usage() {
fmt.Fprintln(os.Stderr, `helmd — agent-helm: styr CLI-agenter i tmux via REST-API + hub
användning:
helmd serve [--listen host:port] [--config fil] starta agent-servern
helmd share [--note text] [--session namn] <fil> dela en fil till klienterna
helmd token skriv ut API-token
helmd hub [--listen host:port] [--config fil] starta hubben (Pi5)
helmd hub setpass <användare> [--password PW] sätt/skapa hub-konto
helmd hub users lista hub-konton
helmd hub agentkey skriv ut agent-nyckeln
helmd ctl <kommando> CLI-styrklient mot hubben
helmd version`)
}
func fatal(err error) {
fmt.Fprintln(os.Stderr, "helmd:", err)
os.Exit(1)
}
// shareFile uploads a file to a running helmd and returns the share id.
func shareFile(server, token, path, note, session string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
var buf bytes.Buffer
mw := multipart.NewWriter(&buf)
fw, err := mw.CreateFormFile("file", filepath.Base(path))
if err != nil {
return "", err
}
if _, err := io.Copy(fw, f); err != nil {
return "", err
}
mw.WriteField("note", note)
mw.WriteField("session", session)
mw.Close()
req, err := http.NewRequest("POST", server+"/api/shares", &buf)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", mw.FormDataContentType())
req.Header.Set("Authorization", "Bearer "+token)
client := &http.Client{Timeout: 60 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if resp.StatusCode >= 300 {
return "", fmt.Errorf("servern svarade %s: %s", resp.Status, bytes.TrimSpace(body))
}
var out struct {
ID string `json:"id"`
}
if err := json.Unmarshal(body, &out); err != nil {
return "", err
}
return out.ID, nil
}