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
This commit is contained in:
252
doc/hub-plan.md
Normal file
252
doc/hub-plan.md
Normal file
@@ -0,0 +1,252 @@
|
||||
# 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 <cmd>` | 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:<agent_key>}` → `{key:<pollnyckel>}`. 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": "<agent_key från hubben>",
|
||||
"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": "<genereras vid första start>",
|
||||
"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 <agent>
|
||||
helmd ctl new <agent> <namn> [--cmd K] [--cwd D]
|
||||
helmd ctl screen <agent> <session> [--ansi] [--history N]
|
||||
helmd ctl prompt <agent> <session> <text …>
|
||||
helmd ctl question|answer|keys|kill <agent> <session> […]
|
||||
helmd ctl shares <agent>
|
||||
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).
|
||||
@@ -60,6 +60,11 @@ webb-PWA (LAN/tunnel) ─────┼──► helmd :8788 (Go, REST+SSE, tok
|
||||
3. ⬜ **Android-klient**: nytt projekt eller Capacitor-port av PWA:n.
|
||||
- Egen SSH-tunnel (t.ex. sshj/JSch) → Pi5 → helmd; token +
|
||||
värdprofil i appen; push via ntfy-appen eller UnifiedPush.
|
||||
4. 🔨 **Hub på Pi5** (påbörjad 2026-08-06): central server som alla
|
||||
agenter ansluter utåt till (delad agent-nyckel, inga konton) och
|
||||
där web-/CLI-klienter loggar in (lokalt admin-konto →
|
||||
`Authenticator`-interface för LDAP senare) och styr alla agenters
|
||||
sessioner. Full design: **[hub-plan.md](hub-plan.md)**.
|
||||
|
||||
## Beslut
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# agent-helm v2 — statusnotering 2026-08-05
|
||||
# agent-helm v2 — statusnotering 2026-08-06
|
||||
|
||||
Var vi står i implementationen. Uppdatera denna fil när läget ändras.
|
||||
(Planen och besluten: [plan.md](plan.md). Full teknisk doc:
|
||||
[../helmd/README.md](../helmd/README.md).)
|
||||
(Planen och besluten: [plan.md](plan.md) + [hub-plan.md](hub-plan.md).
|
||||
Full teknisk doc: [../helmd/README.md](../helmd/README.md).)
|
||||
|
||||
## Läge: steg 1 + 2 klara och deployade, steg 3 (Android) kvar
|
||||
## Läge: steg 1 + 2 klara och deployade; steg 4 (hub) pågår; steg 3 (Android) kvar
|
||||
|
||||
| Del | Status | Var |
|
||||
|-----|--------|-----|
|
||||
@@ -13,6 +13,7 @@ Var vi står i implementationen. Uppdatera denna fil när läget ändras.
|
||||
| **Release-binärer** (x64 + arm64, webbklient inbyggd) | ✅ rullande `helmd-latest` | [releases](https://gitea.brasse-pc.eu/brasse/agent-helm/releases) |
|
||||
| **Docker-image** (arm64, alpine + tmux/bash) | ✅ byggs i CI | `localhost:5000/helmd:latest` |
|
||||
| **Pi5-deploy** (LAN-only, ingen NPM-vhost) | ✅ kör | stack `/srv/dockge-staks/helmd/`, `http://192.168.0.19:8788` |
|
||||
| **Hub** (`helmd hub`: agent-uplink + login + web + `helmd ctl`) | ✅ kod klar, testad lokalt (enhetstester + 15-stegs smoke); Pi5-deploy återstår | [hub-plan.md](hub-plan.md), `helmd/README.md` |
|
||||
| **Android-klient** (egen SSH-tunnel) | ⬜ ej påbörjad | steg 3 i plan.md |
|
||||
| v1 (Node: daemon/server/web + rcai.brasse-pc.eu) | 🟡 kvar orörd tills v2 helt ersatt | `packages/` |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user