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

214 lines
9.7 KiB
Markdown

# helmd — agent-helm v2-servern
En **enskild statisk Go-binär** som äger kommunikationen med en
tmux-session där en CLI-agent (agy, claude, …) kör, och exponerar ett
**REST-API + SSE** som webb- och mobilklienterna pratar med.
Designen bygger på insikten från Gemini-OAuth-döden: att integrera mot
en specifik agents hook-mekanism är skört. `helmd` behandlar i stället
**tmux som sanningskälla** — den läser skärmen, känner igen frågor och
skickar tangenter. Fungerar därför med vilken TUI-agent som helst, och
överlever att helmd själv startas om (sessioner adopteras tillbaka).
```
webb-PWA / Android ──(LAN eller SSH-tunnel)──► helmd :8788 ──tmux──► agy
└──► ntfy.brasse-pc.eu (pushar till mobilen)
```
## Säkerhetsmodell
- **Ingen publik exponering.** Servern binder `127.0.0.1:8788` som
default. På det privata nätet kan man binda `0.0.0.0:8788`
(`helmd serve --listen 0.0.0.0:8788`). Utifrån: SSH-tunnel —
`ssh -L 8788:127.0.0.1:8788 <host>` — eller Android-klientens
inbyggda tunnel (steg 3 i planen).
- **Bearer-token** på alla endpoints. Genereras vid första start,
lagras `0600` i `~/.config/helmd/config.json`, visas med
`helmd token`. Skickas som `Authorization: Bearer <t>`,
`X-Api-Token: <t>` eller `?token=<t>` (för SSE/EventSource och
`<img>`-länkar).
- Delad HTML serveras med `Content-Security-Policy: sandbox` så att
ett delat dokument aldrig kan anropa API:et med klientens token.
## Kommandon
```
helmd serve [--listen host:port] [--config fil] starta agent-servern
helmd share [--note text] [--session namn] <fil> dela en fil till klienterna
helmd token skriv ut API-token
helmd hub [--listen host:port] [--config fil] starta hubben (Pi5)
helmd hub setpass <användare> [--password PW] sätt/skapa hub-konto
helmd hub users lista hub-konton
helmd hub agentkey skriv ut agent-nyckeln
helmd ctl <kommando> CLI-styrklient mot hubben
helmd version
```
## Hubben (`helmd hub`)
Central server (Pi5) som **agenter ansluter utåt till** och där web-
och CLI-klienter **loggar in** och styr alla agenters sessioner. Full
design och beslut: [../doc/hub-plan.md](../doc/hub-plan.md).
- **Agent-anslutning:** ingen inloggning/konto — agenten skickar den
**delade agent-nyckeln** (`agent_key` i hubbens config) vid
registrering. Hämta den med `helmd hub agentkey` på hubben, eller
efter inloggning med `helmd ctl agentkey` / Inställningar-fliken i
webben. Transporten är long-poll över HTTP (`/hub/register`,
`/hub/work`, `/hub/result`, `/hub/events`) — agenten kör jobben mot
sin egen lokala mux, så hela API-tabellen ovan funkar oförändrad
genom hubben. Agenter ser aldrig något om varandra.
- **Klient-login:** `POST /api/login` med användarnamn + lösenord →
Bearer-token. Konton ligger lokalt i hub-konfigen (pbkdf2-sha256);
`Authenticator`-interfacet i `hubauth.go` är utbytespunkten för
LDAP/SSO längre fram. Första starten seedas `admin` (lösenord från
`HELMD_HUB_ADMIN_PASSWORD` eller slumpat — skrivs ut i loggen).
- **Klient-API:** `GET /api/agents` (lista + online-status),
`ANY /api/agents/{namn}/…` (proxas till agentens `/api/…`),
`GET /api/events` (merged SSE, fältet `agent` på varje event),
`GET /api/agentkey`, `GET/POST /api/login|logout|me`.
`GET /api/mode` (oautentiserad) svarar `hub`/`local` så att den
inbäddade webbklienten kan välja login-form — i hub-läge får den
användarnamn/lösenord och en agentväljare.
- **Agentens config** (`config.json`) pekar ut hubben:
```json
"hub": {
"url": "http://192.168.0.19:8790",
"name": "brasse-linux01",
"key": "<agent_key>",
"enabled": true
}
```
Det lokala API:t på `:8788` fortsätter fungera parallellt — hubben
nere betyder bara att uplinken backar och försöker igen.
- **Hubbens config** (`~/.config/helmd/hub.json`, `HELMD_HUB_CONFIG`
överstyr): `listen` (default `127.0.0.1:8790`), `auth` (`local`),
`users`, `agent_key`, `session_ttl_hours` (default 168).
- **CLI-klienten:** `helmd ctl login --server http://192.168.0.19:8790`
→ `ctl agents`, `ctl sessions <agent>`, `ctl new <agent> <namn>`,
`ctl screen|prompt|question|answer|keys|kill <agent> <session>`,
`ctl shares <agent>`, `ctl events`. Sparar server + token i
`~/.config/helmd/ctl.json`.
- **Smoke-test av hela flödet:** `bash scripts/run-hub-smoke.sh`
(hub + agent + ctl lokalt, 15 kontroller).
`helmd share` är **agentens verktyg för att dela filer** (html, md,
png, jpg, …) till klienterna: agenten kör kommandot i sin shell, filen
laddas upp till servern och dyker upp i klientens delningsflik.
Tillåtna filtyper styrs av `share_exts` i konfigen.
## API
Alla svar är JSON om inget annat sägs. Auth krävs överallt.
| Metod & väg | Gör |
|---|---|
| `GET /api/health` | `{ok, version, sessions}` |
| `GET /api/sessions` | lista sessioner (namn, alive, seq, pending question) |
| `POST /api/sessions` | `{name, cmd?, cwd?}` startar agent i ny tmux-session `helm-<name>`; `{name, adopt:true, tmux?}` adopterar befintlig |
| `GET /api/sessions/{n}` | en sessions tillstånd |
| `DELETE /api/sessions/{n}` | döda sessionen |
| `GET /api/sessions/{n}/screen` | pane-text; `?ansi=1` behåller färger, `?history=N` tar med N rader scrollback |
| `POST /api/sessions/{n}/prompt` | `{text, submit?:bool}` klistrar in text (radbrytningar ok) och trycker Enter |
| `GET /api/sessions/{n}/question` | `{question}` — detekterad väntande fråga eller `null` |
| `POST /api/sessions/{n}/answer` | `{option:N}` väljer alternativ N (1-baserat). Numrerade listor väljs med siffertangent, onumrerade navigeras med pil+Enter |
| `POST /api/sessions/{n}/keys` | `{keys:["Escape","Down","Enter"]}` råa tmux-tangenter |
| `POST /api/sessions/{n}/mode` | `{action:"cycle"}` skickar Shift+Tab (lägesbyte i agy/claude) |
| `GET /api/config` / `PUT /api/config` | läs/ändra agentkommando, args, workdir, poll-intervall, ntfy-inställningar (token ändras aldrig via API) |
| `GET /api/shares` / `POST /api/shares` | lista / ladda upp (multipart, fält `file`, `note`, `session`) |
| `GET /api/shares/{id}` / `…/raw` | metadata / själva filen |
| `DELETE /api/shares/{id}` | ta bort delning |
| `POST /api/notify` | `{topic?, title?, message, priority?}` → ntfy; topic måste finnas i `allowed_topics` (`agent-helm`, `claude`) |
| `GET /api/events` | SSE-ström: `screen` (seq), `question`, `session`, `share` |
### Frågedetektering
Pollern läser panen (default var 500:e ms), och `DetectQuestion`
känner igen agy:s frågeformer: godkännande-listor (`1. Yes … 4. No`),
trust-/val-listor med `>`-markör, och gamla Gemini-CLI:s inramade
pickers. Varje fråga får en stabil hash — servern ntfy:ar **en gång
per ny fråga** till topic `agent-helm` (kan stängas av med
`notify_on_question:false`).
Verifierat mot riktiga agy 1.1.9-sessioner (trust-prompt,
kommandogodkännande) och mot mock i integrationstestet.
## Konfiguration
`~/.config/helmd/config.json` (skapas vid första start; override med
`HELMD_CONFIG` eller `--config`):
```json
{
"listen": "127.0.0.1:8788",
"token": "<genereras>",
"agent_cmd": "agy",
"agent_args": [],
"workdir": "/home/brasse",
"share_dir": "~/.local/share/helmd/shares",
"share_exts": [".html", ".htm", ".md", ".txt", ".json", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".pdf"],
"poll_ms": 500,
"cols": 200, "rows": 50,
"ntfy": {
"server": "https://ntfy.brasse-pc.eu",
"topic": "agent-helm",
"allowed_topics": ["agent-helm", "claude"],
"enabled": true,
"notify_on_question": true
}
}
```
## Webbklienten
Inbäddad i binären (`web/` + `go:embed`) och serveras på `/` — surfa
till servern (t.ex. `http://127.0.0.1:8788` genom en SSH-tunnel) och
klistra in din token. Ramverksfri (ren ES-moduler): sessionslista,
ANSI-färgad skärmvy (SSE + poll), promptfält, frågekort med knappar,
tangent-verktygsrad, delningsflik (bilder, sandboxad HTML, markdown)
och inställningar. Statiska filer kräver ingen auth (koden är publik);
allt data går genom det token-skyddade API:et.
## Docker på Pi5 (LAN-only)
`Dockerfile` bygger en arm64-image med tmux+bash via
`.gitea/workflows/helmd-image.yaml` → `localhost:5000/helmd:latest`.
Stack: `/srv/dockge-staks/helmd/` med host-port **8788** och volym
`/srv/docker/helmd → /data`. **Ingen NPM-vhost** — nås bara på LAN
(`http://192.168.0.19:8788`) eller via SSH-tunnel utifrån. Sessionerna
kör i containern (bash tills en agent installeras där).
## Bygga & testa
```bash
cd helmd
go test ./... # enhetstester (frågedetektorn m.m.)
CGO_ENABLED=0 go build -o build/helmd .
bash scripts/run-integration.sh # 26 API-tester i isolerad Docker-container
```
Integrationstestet startar helmd i en `debian:stable-slim`-container
med en mock-agent (`testdata/mock-agent.sh`) som härmar agy:s båda
frågetyper, och kör hela API-ytan med curl.
## CI / release
`.gitea/workflows/helmd-release.yaml` kör på push till master som rör
`helmd/`: `go vet` + `go test`, bygger **linux x64 + arm64** statiskt
och publicerar på den rullande releasen **`helmd-latest`**
(`helmd-linux-x64`, `helmd-linux-arm64`, `checksums.txt`).
## Kända begränsningar (v1)
- `mode cycle` skickar Shift+Tab (BTab) — verifierat tangentnamn i
tmux men inte bekräftat att agy byter läge på det; justera via
`/keys` om agy använder annan tangent.
- Ingen WebSocket-terminal ännu — klienterna pollar `screen` +
lyssnar på SSE. Räcker för kort-UI:t; rå terminal-vy kommer med
webbklienten (steg 2).
- En helmd-instans per maskin/användare; flera agenter = flera
sessioner i samma helmd.