Files
agent-helm/README.md
Bjorn Blomberg 94259b3456
All checks were successful
build-and-push / build (push) Successful in 18s
release-client / build-release (push) Successful in 48s
auth: OIDC/lokal inloggning + device-flow för klienter, sessioner per användare
Ersätter den delade AGENT_HELM_TOKEN med en riktig auth-modell:

- Webben loggar in med lokala konton ELLER OIDC (Authentik/Keycloak/…), valt
  per server. Inloggning ger en HMAC-signerad cookie; WS autentiseras via cookien.
- Klienten (daemon) kopplas via device-flödet (RFC 8628-likt): `agent-helm connect`
  skriver ut en URL, du loggar in + godkänner i webben, och får ett klient-token
  som identifierar din användare. Token sparas i ~/.config/agent-helm/.
- Sessioner är per användare (web ser bara sina egna; admin ser alla). Flera
  klient-instanser = flera parallella sessioner; --new tvingar ny, annars resume.
- Identitet är utbytbar (lokal/OIDC) men device-flödet + sessionerna är alltid
  agent-helms egna → funkar för vem som helst som hostar detta, med eller utan IdP.
- Bootstrap-admin via env (engångs) eller first-run-setup i UI:t. Klient-creds
  kan listas/återkallas; minimal admin-UI (skapa användare, konfigurera OIDC).

Nya filer: server/store.ts (JSON-store, scrypt, cookies), server/oidc.ts
(auth-code+PKCE), web/lib/api.ts. Verifierat end-to-end lokalt (login → device
→ approve → daemon-WS → web ser sessionen; web utan cookie nekas).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U68YyHsxU91ecsen84WQV4
2026-06-29 01:21:29 +02:00

229 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
> Fjärrstyr CLI-agent-sessioner (Gemini CLI först) från mobil eller webb — med en
> alltid-fungerande terminalkanal i botten och snygga, strukturerade frågekort som
> bonus ovanpå.
`agent-helm` låter dig köra en agent (Gemini CLI) på valfri maskin, strömma dess
session till en server på din Pi5, och styra/svara på den från en webbfrontend
(installbar PWA) i mobilen. Du ser alla anslutna instanser på ett ställe och kan
hoppa in i vilken som helst, var du än är.
---
## Designprinciper
1. **Graceful degradation — terminalen funkar alltid.**
Den råa terminalkanalen (tangenttryck → pty) är *baslinjen* och fungerar oavsett
konfiguration. Hooks som ger snygga frågekort är *progressive enhancement* — om en
`BeforeTool`/`Notification`-hook saknas, är felkonfigurerad eller kraschar, kan du
fortfarande styra sessionen fullt ut genom att skicka **piltangenter + Enter**
(och all annan text) rakt in i terminalen. Se [Fallback-navigering](#fallback-navigering).
2. **Två tydligt åtskilda input-vägar.** Fritext/tangenttryck går till pty:ns stdin.
Strukturerade hook-svar (allow/deny) går tillbaka via hookens returvärde — aldrig
genom att "skriva y i terminalen". Se [Input-kanaler](#input-kanaler).
3. **pty äger agenten, inte tmux.** Appen kör Gemini i en `node-pty`. Persistens är ett
*valfritt yttre lager* (tmux eller Docker) — inte en del av integrationen.
4. **Servern äger sessionens minne.** En scrollback-buffert per session gör att en
mobil som återansluter ser var den är, inte en tom skärm.
---
## Komponenter
| Komponent | Roll | Teknik |
|---|---|---|
| **daemon** (`packages/daemon`) | "Appen". Spawnar Gemini i en `node-pty`, strömmar pty-bytes utåt över WebSocket, tar emot input och skriver till pty:n. Levererar även hook-frågor och släpper dem när svar kommer. | Node.js, `node-pty`, `ws` |
| **server** (`packages/server`) | Control-plane på Pi5. Session-registry (daemon ↔ session-id), WebSocket-mux mot både daemons och frontends, scrollback per session (headless `xterm` + `SerializeAddon`), routing av input till rätt session. | Node.js, `ws`, `@xterm/headless` |
| **web** (`packages/web`) | PWA-frontend. `xterm.js` för konversationen, kort-vy för hook-frågor, input-toolbar för fallback-navigering, lista över alla anslutna instanser. | Vue 3, Vite, `@xterm/xterm`, PWA |
| **hooks** (`hooks/`) | Gemini CLI-hooks (`BeforeTool`, `Notification`) som postar frågan till daemonen och long-pollar tills ett svar kommer. | Shell/Node-script, `~/.gemini/settings.json` |
---
## Repo-struktur
```
agent-helm/
├── README.md
├── .gitignore
├── package.json # workspace-rot (npm/pnpm workspaces)
├── packages/
│ ├── daemon/ # node-pty-värd för Gemini + WS-klient mot servern
│ │ ├── src/
│ │ └── package.json
│ ├── server/ # Pi5 control-plane: WS-mux, registry, scrollback
│ │ ├── src/
│ │ └── package.json
│ └── web/ # Vue 3 PWA: xterm.js + kort-vy + fallback-input
│ ├── src/
│ └── package.json
├── hooks/
│ ├── before-tool.sh # postar verktygsförfrågan till daemonen, blockar
│ └── notification.sh # postar agent-notiser (t.ex. "väntar på input")
└── docker/
├── daemon.Dockerfile
├── server.Dockerfile # byggs och pushas till 192.168.0.19:5000
└── compose.pi5.yml # server + ntfy, restart: unless-stopped
```
---
## Dataflöde
```
[daemon / "appen"] (körs valfritt i tmux eller Docker för persistens)
node-pty ── spawnar ── gemini (interaktivt TUI)
│ └─ BeforeTool/Notification-hook (körs HÄR, daemon-side)
│ pty-bytes (stdout) │ POST {tool_name, tool_input}, blockar tills svar
▼ ▼
───── WebSocket (utåtgående) ──────────────────► [Pi5-server / control-plane]
◄──── input (text / tangenter / hook-svar) ────── • session-registry: daemon ↔ session-id
• headless xterm + SerializeAddon → scrollback
• router:
text/tangenter → pty.write()
hook-svar → släpper blockad hook
WebSocket ▼
[Vue 3 PWA]
• lista över alla anslutna instanser
• xterm.js → konversationen (rå, alltid)
• kort-vy → hook-frågor (allow/deny)
• toolbar → ↑↓←→ Enter Esc Tab Ctrl-C
```
**Steg för steg (en verktygsförfrågan):**
1. Gemini vill köra ett verktyg och fyrar `BeforeTool`-hooken på daemon-sidan.
2. Hooken POST:ar `{tool_name, tool_input, session_id}` till daemonen och **blockar**
(long-poll) tills ett beslut kommer.
3. Daemonen vidarebefordrar frågan till servern → servern pushar till alla frontends
som tittar på sessionen (och triggar en ntfy-push till mobilen).
4. Du trycker **Tillåt** / **Neka** i kort-vyn → frontend → server → daemon →
hooken släpps och returnerar `{"decision":"allow"}` → Gemini fortsätter.
5. **Om hooken inte är uppsatt/fungerar:** inget kort dyker upp. Istället visar Gemini
sin egen y/n-prompt i terminalen, och du svarar via fallback-navigeringen (se nedan).
---
## Input-kanaler
Två vägar, alltid åtskilda:
| Input | Väg | Hamnar i |
|---|---|---|
| Fritext, piltangenter, Enter, Esc, Tab, Ctrl-C | frontend → server → daemon → `pty.write()` | terminalens **stdin** |
| Svar på hook-fråga (allow / deny / val) | frontend → server → **släpper den blockade hooken** | hookens **returvärde** |
> ⚠️ Strukturerade godkännanden skickas *aldrig* genom att skriva i terminalen — det
> skapar race mot TUI:ns egen prompt. De går tillbaka via hookens returvärde.
---
## Fallback-navigering
Detta är ett **förstklassigt krav**, inte en reservplan: frontenden ska alltid kunna
styra sessionen även om ingen hook är inblandad.
- Frontenden har en **input-toolbar** med `↑ ↓ ← →`, `Enter`, `Esc`, `Tab`, `Ctrl-C`
(plus snabbknappar `y`/`n`). Dessa skickar motsvarande **escape-sekvenser** in i pty:n
via `pty.write()` (t.ex. piltangent upp = `\x1b[A`, Enter = `\r`).
- `xterm.js` vidarebefordrar dessutom riktiga tangenttryck från ett fysiskt tangentbord
rakt till samma kanal.
- Resultat: när Gemini visar sin **inbyggda** TUI-prompt (verktygsgodkännande, val i en
meny, bekräftelse) kan du navigera och svara helt utan hooks. Hooks gör bara frågorna
snyggare — de är aldrig en förutsättning för att kunna styra sessionen.
Praktiskt innebär det att man kan börja använda systemet **innan** några hooks är
konfigurerade, och att en trasig hook aldrig låser en session.
---
## Persistens
- **Process-persistens (valfritt):** kör daemonen i `tmux` för ad-hoc ("överlever att
jag stänger SSH/laptop"), eller som **Docker-container med `restart: unless-stopped`**
/ systemd-unit för alltid-på på Pi5. tmux är inte en del av integrationen — bara ett
yttre skal.
- **Vy-persistens (alltid på):** servern håller en scrollback-buffert per session via en
headless `xterm` + `SerializeAddon`. En frontend som ansluter (eller en mobil som
väcks ur viloläge) får en ögonblicksbild + live-svans, inte en tom skärm.
---
## Åtkomst, auth & notiser (Pi5)
- **Auth:** ingen delad token. **Webben** loggar in med **lokala konton** *eller*
**OIDC** (Authentik/Keycloak/…) — väljs per server. **Klienten** kopplas via
**device-flödet**: den skriver ut en URL, du loggar in och godkänner i webben, och
får då ett klient-token som identifierar din användare. Sessioner är per användare;
klienter kan återkallas individuellt. Första admin seedas via
`AGENT_HELM_ADMIN_USER/PASSWORD` (engångs) eller en first-run-setup i UI:t.
- **Åtkomst:** primärt via **Tailscale** (inget exponerat publikt). Alternativt via din
NPM-reverse-proxy på t.ex. `agent.brasse-pc.eu` (TLS terminerar i NPM → Secure-cookie).
- **Notiser:** self-hostad **ntfy** på Pi5 pushar till mobilen när en session väntar på
input. Servern postar till ett ntfy-topic; PWA:n / ntfy-appen tar emot.
- **Registry:** server- och daemon-images byggs och pushas till `192.168.0.19:5000`.
---
## Teknikstack
- **Node.js** överallt på backend (`node-pty`, `ws`, `@xterm/headless`).
- **Vue 3 + Vite** för frontend, `@xterm/xterm` för terminalrendering, PWA för
mobil-installation och push.
- **Docker** (compose) för Pi5-deploy, **ntfy** för push, **Tailscale** för åtkomst.
- Mål-agent: **Gemini CLI** (headless/hooks), men daemon-lagret är agent-agnostiskt och
kan wrappa vilken pty-baserad CLI-agent som helst.
---
## Kom igång (utveckling)
> **OBS NTFS-disk:** projektet ligger på en NTFS-formaterad disk
> (`/run/media/brasse/Exton-speed-game`). `pnpm` **hänger** på `ntfs3`-drivern vid den
> tunga småfils-I/O:n när den extraherar/hardlinkar i storen. Lägg därför pnpm-storen
> **och** den virtuella storen på ett Linux-filsystem (btrfs/ext4) vid installation —
> bara källkoden ligger kvar på NTFS. På en Linux-fs (t.ex. Pi5) behövs inga flaggor.
```bash
# Installera på NTFS-workstationen (store + virtual-store på btrfs):
pnpm install \
--store-dir /home/brasse/.cache/agent-helm/store \
--virtual-store-dir /home/brasse/.cache/agent-helm/vstore
# (På vanlig Linux-fs räcker: pnpm install)
# Konfig: kopiera och sätt bootstrap-admin (lokalt läge)
cp .env.example .env # sätt AGENT_HELM_ADMIN_USER/PASSWORD (engångs)
# Kör de tre delarna i var sin terminal:
pnpm dev:server # control-plane på :8787
pnpm dev:web # Vite-dev-server (proxar /api + /ws → :8787)
# I en tredje terminal: logga in klienten + kör agenten
pnpm --filter @agent-helm/daemon start -- connect --server http://localhost:5173
```
1. Öppna Vite-länken, logga in som admin (eller kör first-run-setupen).
2. Daemonen skriver ut en URL — öppna den, godkänn klienten.
3. Daemonen ansluter och dyker upp i din session-lista. Styr den i webben.
Sätt `AGENT_CMD=bash` för daemonen om du vill testa pipelinen utan Gemini-auth.
## Status
**Tunn vertikal slice klar och verifierad.** Token-auth, daemon-registrering,
session-lista, scrollback-snapshot och `input → pty → output` fungerar end-to-end
(rök-testat med server ↔ daemon ↔ klient). Alla paket typecheckar; frontenden bundlar.
### Roadmap
- [x] Monorepo-uppsättning (pnpm workspaces) + paket
- [x] `daemon`: node-pty spawnar agenten, strömmar pty över WS (auto-reconnect)
- [x] `server`: session-registry + WS-mux + token-auth + scrollback (SerializeAddon)
- [x] `web`: xterm.js-vy + input-toolbar (fallback-navigering) + instans-lista
- [ ] `hooks`: `before-tool` long-poll → kort-vy i frontend (strukturerade godkännanden)
- [ ] ntfy-push + Tailscale-/NPM-deploy på Pi5
- [ ] Docker-images → `192.168.0.19:5000`