diff --git a/README.md b/README.md index 5f09298..046bf98 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ nooit zelf. │ │ │ ┌─ container: agent-runner ────────────────────────────────┐ │ │ │ PID 1: tini → run-agent.sh (daemon-loop) │ │ -│ │ ├─ health-server.js (8080 → host) │ │ +│ │ ├─ health-server.js (8080 → host 18080) │ │ │ │ └─ claude -p (per-batch, met MCP via stdio) │ │ │ │ └─ scrum4me-mcp → Neon Postgres │ │ │ │ │ │ @@ -52,16 +52,30 @@ fouten. | `bin/check-tokens.sh` | Pre-flight: API-token, OAuth-token, DB-bereikbaarheid | | `bin/job-prepare.sh` | Per-job: bare-fetch + clone-via-reference naar `/tmp/job-` | | `bin/job-cleanup.sh` | Per-job: logs naar `/var/log`, working tree weg | -| `bin/health-server.js` | HTTP-endpoint op 8080 dat state.json en marker-files leest | +| `bin/health-server.js` | HTTP-endpoint op 8080 (intern) dat state.json en marker-files leest | | `bin/rotate-logs.sh` | Compress/cleanup van oude `.log`-bestanden | | `.env.example` | Alle env-vars met uitleg | ## Vereisten op de NAS - Container Station 2+ (Docker compose v2) -- Drie shares aangemaakt: `/share/Agent/cache`, `/share/Agent/logs`, `/share/Agent/state` -- Of één share `/share/Agent` waaronder de drie subdirs vallen -- Internet-uitgang naar `api.anthropic.com`, `github.com`, je Neon-host, `registry.npmjs.org` +- **`Agent` als QTS Shared Folder** op een echte volume (bv. `CACHEDEV1_DATA`). + Niet een `mkdir /share/Agent` — `/share` zelf is een 16 MB tmpfs en handmatige + directories overleven geen reboot. Aanmaken via Control Panel → Privilege → + Shared Folders → Create. QTS legt dan automatisch de symlink + `/share/Agent → /share/CACHEDEV1_DATA/Agent`. +- Drie subdirs onder die share: `/share/Agent/cache`, `/share/Agent/logs`, + `/share/Agent/state`. Aanmaken via File Station of via SSH na share-creatie. +- Internet-uitgang naar `api.anthropic.com`, `github.com`, je Neon-host, `registry.npmjs.org`. + +> **Verifieer** vóór je deployt dat `/share/Agent` echt op disk staat: +> ```bash +> ssh admin@ 'ls -la /share/ | grep Agent; df -h /share/Agent' +> ``` +> Verwacht een symlink (`l...Agent -> /share/CACHEDEV1_DATA/Agent`) en een +> df-uitvoer met TB-grootte op `cachedev1`/`cachedev2`. Als je hier `tmpfs 16M` +> ziet, is de share geen geregistreerde QTS Shared Folder en zal elke transfer +> >16 MB falen met `scp: write remote ... Failure`. ## Deploy @@ -95,10 +109,130 @@ docker compose build docker compose up -d # 5. Verifiëren -curl http://nas.local:8080/health +curl http://nas.local:18080/health docker compose logs -f ``` +> **QNAP-port:** host-poort 8080 is bezet door de QTS-webinterface; daarom +> mapt deze stack standaard `18080:8080`. Override via +> `AGENT_HEALTH_PORT_HOST` in `.env` als je een andere host-poort wilt. + +## Deploy — cross-build vanaf Mac (Apple Silicon → amd64-NAS) + +Alternatief voor de in-place build hierboven. Bouw de image op je Mac voor +`linux/amd64`, schrijf 'm naar een tarball, transfer naar de NAS en laad daar. +Handig als de NAS te langzaam is om te builden (npm install op een N5095 met +NAS-storage is traag) of als je geen `git push` wilt voor elke iteratie. + +**Vóór je begint:** controleer dat `/share/Agent` een echte QTS Shared Folder is +(zie [Vereisten op de NAS](#vereisten-op-de-nas)). Dat is de meest voorkomende +val. + +### 1. Tokens en `.env` op je Mac + +Zelfde tokens als in [Deploy](#deploy). Hou er rekening mee dat je **twee +`.env`-bestanden** kunt willen: één voor lokaal Mac-testen +(`AGENT_PLATFORM=linux/arm64`, `AGENT_UID=501`, paths onder `/Users/...`) en +één voor NAS-runtime. De NAS-versie wordt bij stap 4 ge-scp'd en met `sed` +geschikt gemaakt. + +### 2. Image bouwen voor amd64 + +```bash +cd /Users//Development/scrum4me-docker + +docker buildx build \ + --platform linux/amd64 \ + --build-arg MCP_GIT_REF=main \ + --build-arg CLAUDE_CODE_VERSION=latest \ + --build-arg AGENT_UID=1000 \ + --build-arg AGENT_GID=1000 \ + -t scrum4me-agent-runner:local \ + --load \ + . +``` + +`AGENT_UID/GID=1000` zijn de **NAS-UIDs**, niet je Mac-UIDs (vaak `501/20`). +Bij verkeerde UIDs kan de container niet schrijven naar de bind-mounts. + +Verifieer architectuur: + +```bash +docker image inspect scrum4me-agent-runner:local --format '{{.Architecture}}' +# verwacht: amd64 +``` + +### 3. Image naar tarball + +```bash +docker save scrum4me-agent-runner:local | gzip > scrum4me-agent-runner-amd64.tar.gz +shasum -a 256 scrum4me-agent-runner-amd64.tar.gz +``` + +De tarball is ~580 MB voor de huidige image-size. Hij staat in `.gitignore`, +dus geen risico op accidenteel committen. + +### 4. NAS voorbereiden + bestanden transferren + +```bash +# .env naar de NAS (de Mac-versie; we patchen path-velden zo direct) +scp .env admin@:/share/Agent/scrum4me-agent-runner/.env + +# image-tarball + bijgewerkte compose +scp scrum4me-agent-runner-amd64.tar.gz docker-compose.yml package.json README.md \ + admin@:/share/Agent/scrum4me-agent-runner/ +``` + +Zet de NAS-specifieke waarden in `.env`: + +```bash +ssh admin@ " + source /etc/profile + cd /share/Agent/scrum4me-agent-runner + sed -i \ + -e 's|^NAS_BASE=.*|NAS_BASE=/share/Agent|' \ + -e 's|^AGENT_BASE=.*|AGENT_BASE=/share/Agent|' \ + -e 's|^AGENT_PLATFORM=.*|AGENT_PLATFORM=linux/amd64|' \ + -e 's|^AGENT_UID=.*|AGENT_UID=1000|' \ + -e 's|^AGENT_GID=.*|AGENT_GID=1000|' \ + -e 's|^AGENT_HEALTH_PORT_HOST=.*|AGENT_HEALTH_PORT_HOST=18080|' \ + .env + chmod 600 .env +" +``` + +> **`source /etc/profile` is verplicht** in non-interactieve ssh op QNAP. Zonder +> die regel staat `docker` niet op `$PATH` (Container Station's binary zit onder +> `/share/CACHEDEV*_DATA/.qpkg/container-station/...`) en faalt elk +> `docker`-commando met `command not found`. + +### 5. Image laden + container recreaten + +```bash +ssh admin@ " + source /etc/profile + set -e + cd /share/Agent/scrum4me-agent-runner + + # checksum-verificatie (optioneel maar aan te raden) + sha256sum scrum4me-agent-runner-amd64.tar.gz + + # image laden — overschrijft bestaande :local tag + gunzip -c scrum4me-agent-runner-amd64.tar.gz | docker load + + # container recreate; --no-build voorkomt onbedoelde NAS-side build + docker compose up -d --no-build --force-recreate agent + + docker compose ps + curl -fsS http://localhost:18080/health | head -c 800 +" +``` + +Verwacht in de health-output `cache_free_bytes` met een groot getal +(TB-orde) — dat is je signaal dat `/var/cache` op echte disk zit. +Een waarde rond `16 MB` (`16777216`) betekent dat je per ongeluk +nog steeds op tmpfs draait. + ## Updaten (handmatig, bewust) `SCRUM4ME_TOKEN` of `CLAUDE_CODE_OAUTH_TOKEN` rouleer je via een rebuild: @@ -136,7 +270,7 @@ docker exec scrum4me-agent df -h /var/cache ## Health-endpoint -`GET http://:8080/health` retourneert: +`GET http://:18080/health` retourneert: ```json { @@ -178,6 +312,114 @@ Push gaat over dezelfde token: `git push -u origin feat/story-` slaagt zonder prompt. `gh pr create` (voor producten met `auto_pr=true`) gebruikt dezelfde `GH_TOKEN` via de `gh` CLI's standaard env-detect. +## Veelvoorkomende issues + +Verzameld uit echte deploy-sessies op een TS-664 met QTS 5.x. Eerst checken +voor je gaat tweaken. + +### `/share/Agent` is `tmpfs 16M` — scp faalt op grote files + +**Symptoom:** +``` +scp: write remote ".../scrum4me-agent-runner-amd64.tar.gz": Failure +``` +Eerst 100 % bij kleine files, dan blijvend "Failure" zodra een file de tmpfs +vol maakt. + +**Oorzaak:** `/share/Agent` is geen geregistreerde QTS Shared Folder maar een +gewone directory in de root-tmpfs. Na elke reboot is alle inhoud weg en is +`/share/Agent` slechts een entry in de 16 MB `/share`-tmpfs. + +**Fix:** maak `Agent` aan via Control Panel → Privilege → Shared Folders → +Create. Daarna verschijnt automatisch `lrwxrwxrwx /share/Agent → /share/CACHEDEV1_DATA/Agent`. +Als die symlink ontbreekt omdat er al een tmpfs-directory `/share/Agent` staat: + +```bash +ssh admin@ " + source /etc/profile + docker stop scrum4me-agent 2>/dev/null + docker rm scrum4me-agent 2>/dev/null + rm -rf /share/Agent + ln -s /share/CACHEDEV1_DATA/Agent /share/Agent + mkdir -p /share/Agent/{cache,logs,state,scrum4me-agent-runner} + ls -la /share/ | grep Agent # moet nu een symlink tonen +" +``` + +> **`rm -rf /share/Agent` is veilig** zolang `/share/Agent` nog tmpfs is — +> alle content stond in RAM en zou de volgende reboot toch verdwenen zijn. +> Maar verifieer eerst met `df -h /share/Agent` dat 't echt tmpfs is. + +### `docker: command not found` in non-interactieve ssh + +**Symptoom:** +```bash +ssh admin@ 'docker ps' +# sh: line 1: docker: command not found +``` + +**Oorzaak:** QNAP's `admin`-user heeft `docker` op `$PATH` via +`/etc/profile.d/*.sh` van Container Station. Login-shells laden die scripts; +non-interactieve `ssh user@host 'cmd'` doet dat **niet**. + +**Fix:** `source /etc/profile` aan het begin van je remote command: + +```bash +ssh admin@ "source /etc/profile && docker ps" +``` + +### `yaml: control characters are not allowed` + +**Symptoom:** +```bash +docker compose down +# yaml: control characters are not allowed +``` + +**Oorzaak:** een eerdere `scp` van `docker-compose.yml` faalde halverwege en +liet een file met NUL-bytes / partial writes achter. De file-grootte klopt +maar de inhoud is corrupt. + +**Fix:** scp opnieuw vanaf je werkstation. Voor het stoppen van een al- +draaiende container heb je de yml niet nodig: + +```bash +docker stop scrum4me-agent && docker rm scrum4me-agent +``` + +Daarna `scp docker-compose.yml admin@:/share/Agent/scrum4me-agent-runner/` +en pas dan `docker compose up -d --no-build --force-recreate`. + +### QTS-console-menu Python-traceback bij ssh-commando + +**Symptoom:** lange Python-traceback over `consolemenu_q/prompt_utils.py:31` +en `Inappropriate ioctl for device`, gevolgd door je daadwerkelijke output. + +**Oorzaak:** QTS' admin-shell start een Python-TUI (qts-console-menu) die +crasht zonder echte TTY. Niet-fataal — je commando wordt alsnog gerund. + +**Fix:** negeer de traceback. Als je 'm écht weg wilt, gebruik +`ssh -tt admin@ '...'` voor een geforceerde pseudo-TTY, maar pas op: +sommige scripts hangen daarop omdat ze interactie verwachten. + +### `.env` is weg na `rm -rf /share/Agent` + +**Symptoom:** na het opruimen van een tmpfs-`/share/Agent` weigert +`docker compose up`: +``` +env file /share/Agent/scrum4me-agent-runner/.env not found +``` + +**Oorzaak:** `.env` zit in `.gitignore` en wordt nooit door `git pull` of `scp +.` automatisch teruggezet. Bij het wegnuken van een corrupte share verdwijnt-ie +mee. + +**Fix:** scp 'm vanaf je werkstation, en patch path-velden op de NAS-zijde +(zie [Deploy — cross-build](#deploy--cross-build-vanaf-mac-apple-silicon--amd64-nas) +stap 4). Zorg dat je `.env` op je werkstation **alle** secrets bevat — vooral +`SCRUM4ME_TOKEN`, dat in een Mac-only `.env` makkelijk ontbreekt omdat +lokale tests soms zonder Scrum4Me-API draaien. + ## Bekende grenzen - **Eén actieve job tegelijk.** De wrapper-loop is sequentieel. Voor diff --git a/docker-compose.yml b/docker-compose.yml index ac31ce1..f12b762 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,7 +25,7 @@ services: - /tmp:size=4g,mode=1777 ports: - - "${AGENT_HEALTH_PORT_HOST:-8080}:8080" + - "${AGENT_HEALTH_PORT_HOST:-18080}:8080" restart: unless-stopped diff --git a/package.json b/package.json index ee24c53..3b2c1b3 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "down": "docker compose down", "logs": "docker compose logs -f", "rebuild": "docker compose build --no-cache && docker compose up -d", - "health": "curl -fsS http://localhost:${AGENT_HEALTH_PORT_HOST:-8080}/health | jq ." + "health": "curl -fsS http://localhost:${AGENT_HEALTH_PORT_HOST:-18080}/health | jq ." }, "engines": { "node": ">=22"