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
161 lines
3.8 KiB
Go
161 lines
3.8 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
type NtfyConfig struct {
|
|
Server string `json:"server"`
|
|
Topic string `json:"topic"` // server events (questions etc.)
|
|
AllowedTopics []string `json:"allowed_topics"` // POST /api/notify may only use these
|
|
Enabled bool `json:"enabled"`
|
|
NotifyOnQuestion bool `json:"notify_on_question"`
|
|
}
|
|
|
|
// HubUplink pekar ut vilken hub agenten ska ansluta till. Anslutningen
|
|
// sker utåt (agenten ringer hubben) utan konto — hubben litar på den
|
|
// delade agent-nyckeln (hämtas med `helmd hub agentkey` på hubben
|
|
// eller efter inloggning i webben/CLI:n).
|
|
type HubUplink struct {
|
|
URL string `json:"url"` // t.ex. http://192.168.0.19:8790
|
|
Name string `json:"name"` // agentens namn i hubben (tomt = hostname)
|
|
Key string `json:"key"` // hubbens agent_key
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
type Config struct {
|
|
Listen string `json:"listen"`
|
|
Token string `json:"token"`
|
|
AgentCmd string `json:"agent_cmd"`
|
|
AgentArgs []string `json:"agent_args"`
|
|
Workdir string `json:"workdir"`
|
|
ShareDir string `json:"share_dir"`
|
|
ShareExts []string `json:"share_exts"`
|
|
PollMs int `json:"poll_ms"`
|
|
Cols int `json:"cols"`
|
|
Rows int `json:"rows"`
|
|
Ntfy NtfyConfig `json:"ntfy"`
|
|
Hub HubUplink `json:"hub"`
|
|
|
|
path string
|
|
}
|
|
|
|
func defaultConfig() *Config {
|
|
home, _ := os.UserHomeDir()
|
|
return &Config{
|
|
Listen: "127.0.0.1:8788",
|
|
AgentCmd: "agy",
|
|
AgentArgs: []string{},
|
|
Workdir: home,
|
|
ShareDir: filepath.Join(home, ".local/share/helmd/shares"),
|
|
ShareExts: []string{".html", ".htm", ".md", ".txt", ".json", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".pdf"},
|
|
PollMs: 500,
|
|
Cols: 200,
|
|
Rows: 50,
|
|
Ntfy: NtfyConfig{
|
|
Server: "https://ntfy.brasse-pc.eu",
|
|
Topic: "agent-helm",
|
|
AllowedTopics: []string{"agent-helm", "claude"},
|
|
Enabled: true,
|
|
NotifyOnQuestion: true,
|
|
},
|
|
Hub: HubUplink{
|
|
URL: "",
|
|
Name: hostname(),
|
|
Enabled: false,
|
|
},
|
|
}
|
|
}
|
|
|
|
func configPath() string {
|
|
if p := os.Getenv("HELMD_CONFIG"); p != "" {
|
|
return p
|
|
}
|
|
dir, err := os.UserConfigDir()
|
|
if err != nil {
|
|
dir = "."
|
|
}
|
|
return filepath.Join(dir, "helmd", "config.json")
|
|
}
|
|
|
|
// loadConfig reads the config file, creating it with defaults (and a
|
|
// fresh token) on first run.
|
|
func loadConfig(path string) (*Config, error) {
|
|
cfg := defaultConfig()
|
|
cfg.path = path
|
|
data, err := os.ReadFile(path)
|
|
if os.IsNotExist(err) {
|
|
cfg.Token = newToken()
|
|
if err := cfg.Save(); err != nil {
|
|
return nil, err
|
|
}
|
|
fmt.Fprintf(os.Stderr, "helmd: skapade %s (ny token genererad)\n", path)
|
|
return cfg, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(data, cfg); err != nil {
|
|
return nil, fmt.Errorf("%s: %w", path, err)
|
|
}
|
|
if cfg.Token == "" {
|
|
cfg.Token = newToken()
|
|
if err := cfg.Save(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func (c *Config) Save() error {
|
|
if err := os.MkdirAll(filepath.Dir(c.path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
data, err := json.MarshalIndent(c, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(c.path, append(data, '\n'), 0o600)
|
|
}
|
|
|
|
func hostname() string {
|
|
h, err := os.Hostname()
|
|
if err != nil || h == "" {
|
|
return "agent"
|
|
}
|
|
return h
|
|
}
|
|
|
|
func newToken() string {
|
|
b := make([]byte, 24)
|
|
if _, err := rand.Read(b); err != nil {
|
|
panic(err)
|
|
}
|
|
return hex.EncodeToString(b)
|
|
}
|
|
|
|
func (c *Config) extAllowed(name string) bool {
|
|
ext := strings.ToLower(filepath.Ext(name))
|
|
for _, e := range c.ShareExts {
|
|
if ext == e {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *Config) topicAllowed(topic string) bool {
|
|
for _, t := range c.Ntfy.AllowedTopics {
|
|
if topic == t {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|