# agent-helm — hub-plan (v2 steg 4) *Skriven 2026-08-06, innan implementation. Status hålls i [status.md](status.md); v2-planen i övrigt i [plan.md](plan.md).* ## Mål En **central hub på Pi5** som alla agent-klienter ansluter **utåt** till, och som web- och CLI-klienter loggar in på för att styra **alla** agenters sessioner från ett ställe: ``` brasse-linux01: helmd serve ──utåt──►┐ Pi5-container: helmd serve ──utåt──►├─ helmd hub :8790 (Pi5) (fler maskiner senare) ──────utåt──►┘ ▲ ▲ │ login │ login webbklient helmd ctl (CLI) ``` Krav (från Björn 2026-08-06): 1. En server som agent-klienter ansluter till; styrklienter ansluter till samma server och styr alla agenters sessioner. 2. Agent-klienten ska ha en **configfil** där man ställer in vilken server den ansluter till. 3. Servern **godkänner agent-anslutningar utan inloggning**. 4. Styrklienter (web/CLI) **måste logga in** — i första steget ett **admin-konto**, som längre fram byts mot **LDAP eller annan lösning**. 5. Servern körs på **Pi5**, en **webbklient** kopplad till den, en **agent-klient** på brasse-linux01, och en **CLI-klient** att testa hela flödet med. ## Arkitektur Allt byggs in i **samma helmd-binär** (fortsatt ren stdlib, en fil): | Del | Kommando | Roll | |---|---|---| | Hub | `helmd hub` | Central server på Pi5. Agentregister, proxy, login, merged SSE, webbklient. | | Agent | `helmd serve` | Som idag (tmux-ägare, lokalt REST+SSE) **plus** en utåtgående hub-uplink om `hub` är satt i config.json. | | CLI | `helmd ctl ` | Styrklient mot hubben: login, agents, sessions, prompt, screen, answer, events … | | Web | inbäddad SPA | Samma SPA som idag, utökad: login med användarnamn/lösenord i hub-läge + agentväljare. | ### Transport: long-poll över HTTP, inte WebSocket Agenten ansluter utåt med vanlig HTTP (**long-poll**): den håller 2–3 väntande `GET /hub/work` mot hubben; när en styrklient anropar `/api/agents/{namn}/…` skickas anropet som ett **jobb** i svaret på en väntande poll, agenten kör jobbet mot sin **egen lokala mux** (samma handlers och auth som idag) och POST:ar resultatet tillbaka. Motivering (samma beslut som v2 i övrigt): ren stdlib (inget WS-beroende), curl-testbart, överlever proxies, och all API-logik återanvänds — hubben är bara en växel. ### Hub-API **Agentsidan** (ingen inloggning/konto — men en **delad agent-nyckel**, beslut 2026-08-06, se Auth-modell): | Metod & väg | Gör | |---|---| | `POST /hub/register` | `{name, host, version, key:}` → `{key:}`. Registrerar/ersätter agenten; 401 vid fel agent-nyckel. | | `GET /hub/work?name&key` | long-poll (~25 s) → ett jobb `{id, method, path, content_type, body}` eller 204. | | `POST /hub/result?name&key` | `{id, status, headers, body}` — svar på ett jobb. | | `POST /hub/events?name&key` | `{events:[…]}` — agenten vidarebefordrar sina SSE-events (screen/question/session/share). | **Klientsidan** (login krävs på allt utom `login`/`mode`): | Metod & väg | Gör | |---|---| | `POST /api/login` | `{username, password}` → `{token, user}`. | | `POST /api/logout` / `GET /api/me` | logga ut / vem är jag | | `GET /api/mode` | `{"mode":"hub"}` (oautentiserad — webbklienten avgör läge; agentens serve svarar `"local"`). | | `GET /api/agents` | alla registrerade agenter: namn, host, version, online, last_seen. | | `GET /api/agentkey` | agent-nyckeln (för att koppla in nya agenter — kräver login). | | `ANY /api/agents/{namn}/…` | proxas till agentens lokala `/api/…` — hela befintliga API-ytan (sessions, screen, prompt, question, answer, keys, mode, shares, config). | | `GET /api/events` | merged SSE från alla agenter; varje event får fältet `agent`. Även `agent`-events (online/offline). | `/api/agents/{namn}/events` proxas **inte** (SSE genom jobbkanalen vore en evighetsförfrågan) — hubbens egna `/api/events` ersätter den. Vidarebefordrade svar behåller `Content-Type` **och** `Content-Security-Policy` (delad HTML måste förbli sandboxad även via hubben), `Content-Disposition` och `Cache-Control`. Övriga headers släpps. ### Auth-modell - **Agenter: delad agent-nyckel, inga konton.** (Beslut 2026-08-06 efter säkerhetsdiskussion — helt öppen registrering lät vem som helst på LAN:et ersätta en agent och ta emot prompts.) Hubben genererar en `agent_key` vid första start; agenten anger samma nyckel i sin configfil och skickar den vid registrering — stämmer den litar hubben på agenten. Nyckeln hämtas ur hub-konfigen (`helmd hub agentkey`) **eller** via API/webb/CLI **efter inloggning** (`GET /api/agentkey`). - **Agent-isolering:** `/hub/*`-ytan exponerar ingenting om andra agenter — en agent kan bara polla sina egna jobb, posta sina egna resultat/events (via sin per-registrering-pollnyckel) och får aldrig listor eller data om övriga agenter. Agent→agent-kommunikation finns inte. (Obs: alla maskiner med agent-nyckeln kan registrera valfritt *namn* — nyckelinnehavarna är egna maskiner, så namnkapning skyddas inte mellan dem.) - **Styrklienter: obligatorisk inloggning.** `Authenticator`-interface: ```go type Authenticator interface { Authenticate(username, password string) bool } ``` Första implementation: **lokala konton** i hub-konfigen (pbkdf2-sha256, salt, 210k iterationer). `auth: "local"` i konfigen; LDAP blir en ny implementation + `auth: "ldap"` längre fram — inget annat i hubben behöver ändras. - **Admin-seeding:** första starten utan användare skapar `admin` med lösenord från `HELMD_HUB_ADMIN_PASSWORD` (för Docker) eller ett slumpat som skrivs ut **en gång** i loggen. Byts med `helmd hub setpass admin`. - **Sessionstokens** (Bearer) i minnet, TTL 7 dagar (konfigurerbar). Hub-omstart loggar ut alla — acceptabelt. ### Konfigfiler **Agenten** — `~/.config/helmd/config.json` får en ny sektion (kravet "configfil där man ställer in vilken server man ansluter till"): ```json "hub": { "url": "http://192.168.0.19:8790", "name": "brasse-linux01", "key": "", "enabled": true } ``` Tom `url`/`enabled:false` (default) = dagens beteende, ingen uplink. `name` default = hostname. Lokala API:t fortsätter fungera parallellt (graceful degradation — hubben nere ⇒ styr lokalt som idag). **Hubben** — `~/.config/helmd/hub.json` (`HELMD_HUB_CONFIG`/`--config` överstyr; i containern `/data/hub.json`): ```json { "listen": "127.0.0.1:8790", "auth": "local", "users": [{"username": "admin", "salt": "…", "hash": "…"}], "agent_key": "", "session_ttl_hours": 168 } ``` **CLI:n** — `~/.config/helmd/ctl.json`: `{server, user, token}`, skrivs av `helmd ctl login`. ### CLI-klienten (`helmd ctl`) Testar hela flödet utan webbläsare: ``` helmd ctl login [--server URL] [--user admin] [--password PW] helmd ctl logout | agents | events [--agent X] helmd ctl sessions helmd ctl new [--cmd K] [--cwd D] helmd ctl screen [--ansi] [--history N] helmd ctl prompt helmd ctl question|answer|keys|kill […] helmd ctl shares helmd ctl agentkey # visar hubbens agent-nyckel (kräver login) ``` Lösenord: `--password`, annars läses en rad från stdin (funkar både interaktivt och i pipe för test). ### Webbklienten Samma inbäddade SPA i båda lägena; `GET /api/mode` avgör vid load: - **hub**: loginform användarnamn + lösenord → `/api/login`; **agentväljare** ovanför sessionslistan; alla API-anrop prefixas `/api/agents/{vald agent}`; SSE filtreras på `agent`-fältet; agent-events uppdaterar väljaren. - **local**: exakt dagens beteende (token-inklistring). ## Uppfyllnad av kraven | Krav | Lösning | |---|---| | Server som agenter ansluter till, klienter styr alla sessioner | `helmd hub` + proxy `/api/agents/{namn}/…` + merged SSE | | Configfil på agenten för val av server | `hub`-sektionen i `config.json` | | Agent-anslutning utan inloggning, hubben litar på delad nyckel | `POST /hub/register` med `agent_key` — inga konton; nyckeln hämtas ur hub-config eller efter login (beslut 2026-08-06) | | Agenter isolerade från varandra | `/hub/*` exponerar inget om andra agenter; ingen agent→agent-väg | | Klientlogin: admin-konto nu, LDAP sen | lokala konton + `Authenticator`-interface, `auth`-nyckel i hub-konfig | | Server på Pi5 + webbklient | ny Docker-stack `helm-hub` (:8790), samma image `localhost:5000/helmd`, SPA inbäddad | | Agent-klient på denna dator | `helmd serve` på brasse-linux01 med `hub.enabled:true` | | CLI-klient för att testa flödet | `helmd ctl` | ## Implementationsordning 1. **Kod** (`helmd/`): `hubauth.go` (konton/sessioner/Authenticator), `hub.go` (register/work/result/events + login + proxy + SSE), `uplink.go` (agentens utåtgående anslutning), `ctl.go` (CLI), `main.go` (subkommandon `hub`, `ctl`), `config.go` (`hub`-sektion), `/api/mode` i `api.go`. 2. **Webb** (`helmd/web/`): mode-detektering, login, agentväljare. 3. **Tester**: Go-enhetstester (auth, register/work/result-roundtrip via riktiga uplink-koden mot stub-mux) + lokal e2e-smoke (hub + serve + ctl på brasse-linux01, bash-agent). 4. **Docs**: helmd/README.md (API-tabeller), README, status.md. 5. **Push** → CI bygger `helmd-latest` + arm64-image (serialiserat — max 2 tunga byggen på Pi5-runnern). 6. **Deploy**: stack `helm-hub` på Pi5 (port 8790, volym `/srv/docker/helm-hub → /data`, `HELMD_HUB_ADMIN_PASSWORD` sätts inte — slumpat lösenord läses ur loggen och byts). Agent på brasse-linux01 får `hub`-config. Verifiera med `helmd ctl` mot Pi5 + webbklienten. Ev. koppla även Pi5:s befintliga helmd-container som andra agent (`hub.url` mot hub-containern). 7. **infra-Doc**: uppdatera `services/agent-helm.md` + `hosts/pi5.md` (ny stack/port). ## Beslut - **Samma binär, nya subkommandon** — inte ett nytt repo/paket; följer "hela v2 är EN fil". - **Long-poll i stället för WebSocket** — noll beroenden, curl-testbart. - **Delad agent-nyckel i stället för öppen registrering eller godkännande-flöde** (Björn 2026-08-06) — inga konton på agenterna, men LAN-grannar utan nyckeln kan varken registrera eller kapa agenter. Nyckeln distribueras manuellt via configfilen. - **Hubben är en växel, inte en kopia av API:t** — agentens handlers återanvänds oförändrade via jobb-exekvering mot lokala muxen; nya endpoints i agenten dyker automatiskt upp genom hubben. - **Port 8790** för hubben (8788 = lokal helmd, som idag). - **Delningar bor kvar hos agenten** — hubben proxar även rå-filer; ingen lagring på hubben. - **Sessionstokens i minnet** — enkelt; persistens kan läggas till om utloggning-vid-omstart stör. ## Öppna punkter (tas efter första deployen) - LDAP/Authentik-implementation av `Authenticator` (identity-sso finns redan i infra:n). - Ska Pi5:s helmd-container alltid vara agent mot hubben? (Lutar åt ja när hubben visat sig stabil — då kan LAN-porten 8788 stängas.) - Roller/rättigheter per användare (admin vs läsklient) — allt är admin i första versionen. - ntfy: hubben kunde ta över frågenotiserna så att inte varje agent pushar själv (dubbelnotiser undviks idag genom att bara agenten pushar).