Files
agent-helm/helmd/config.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

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
}