Files
agent-helm/doc/hub-plan.md
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

253 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
23 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).