279 lines
13 KiB
Markdown
279 lines
13 KiB
Markdown
# 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.
|
|
|
|
---
|
|
|
|
## ⚡ v2 pågår: `helmd`
|
|
|
|
Gemini CLI:s OAuth-nedläggning dödade v1-kedjan (daemon → server →
|
|
hooks). **v2** byter fundament: en enskild Go-binär, [`helmd/`](helmd/),
|
|
äger en tmux-session med agenten (**agy**) och exponerar REST+SSE —
|
|
tmux-skärmen är sanningskällan i stället för agent-specifika hooks.
|
|
|
|
- **Status just nu: [`doc/status.md`](doc/status.md)** (vad som är klart,
|
|
deployat, verifierat och vad som återstår)
|
|
- Server + webbklient: klara — se [`helmd/README.md`](helmd/README.md)
|
|
(API, säkerhet, test)
|
|
- **Hub**: central server på Pi5 som alla agenter ansluter utåt till
|
|
(delad agent-nyckel via configfil) och där web-/CLI-klienter loggar
|
|
in och styr allt — design i [`doc/hub-plan.md`](doc/hub-plan.md),
|
|
CLI-klient `helmd ctl`
|
|
- Plan (server → webbklient → Android): [`doc/plan.md`](doc/plan.md)
|
|
- Release: rullande [`helmd-latest`](https://gitea.brasse-pc.eu/brasse/agent-helm/releases)
|
|
(`helmd-linux-x64` + arm64, webbklienten inbyggd)
|
|
|
|
Allt nedanför denna linje beskriver **v1** (Node-stacken i
|
|
`packages/`), som ligger kvar tills webbklienten är portad.
|
|
|
|
---
|
|
|
|
## Skärmbilder (v2, hub-läget)
|
|
|
|
Webbklienten mot hubben — agenter
|
|
ansluter utåt, klienter loggar in och styr allt från ett ställe.
|
|
|
|
**Inloggning** — hub-konto (lokala användare nu, LDAP förberett):
|
|
|
|

|
|
|
|
**Sessioner** — agentväljare till vänster, terminalvy i mitten (här
|
|
kör `fleet status` och `giteactl runs` ur
|
|
[agent-tools](https://gitea.brasse-pc.eu/brasse/agent-tools)),
|
|
tangent-verktygsrad och promptfält:
|
|
|
|

|
|
|
|
**Frågekort** — helmd:s detektor känner igen agentens frågor
|
|
(godkännanden, trust-prompts) och gör dem till knappar; ❓-badgen i
|
|
listan visar var något väntar:
|
|
|
|

|
|
|
|
**Delningar** — agenten delar filer med `helmd share`; bilder, HTML
|
|
(sandboxad) och markdown renderas. Här: en SVG som **agy själv
|
|
genererade med `svgc`** under live-testet:
|
|
|
|

|
|
|
|
---
|
|
|
|
## 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.
|
|
- **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 repot.
|
|
|
|
---
|
|
|
|
## 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:** Repot låg tidigare på en NTFS-disk där `pnpm` **hängde** på `ntfs3`-drivern
|
|
> vid den tunga småfils-I/O:n. Sedan 2026-07-14 ligger alla repon på
|
|
> `/home/brasse/repos/` (btrfs) och `pnpm install` fungerar utan specialflaggor.
|
|
> Flaggorna nedan behövs bara om projektet av någon anledning körs från NTFS igen:
|
|
|
|
```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
|
|
- [x] `hooks`: `before-tool` long-poll → kort-vy i frontend (strukturerade godkännanden)
|
|
- [x] ntfy-push + Tailscale-/NPM-deploy på Pi5
|