Files
felhom.eu/documentation/audits/update-arc-gaps-2026-09-21/00-api-recipe.md
T
admin da20722e76
gates / gates (push) Successful in 27s
Update night 2026-09-21: Phase 0 and Phase 1 evidence, the drill method, and two instrument fixes
INTERIM CHECKPOINT — evidence off the machine at the end of the phase that produced it (R-320),
not at the end of the session. Phases 2-5 follow in a later commit.

Phase 0, all three mechanisms proven with their controls:
- the fleet floor to 0.261.0 with its declared MinAgent — both demo boxes in 13 s, the hub
  logging `managed floor SERVED ... from declared (golden 0.258.0)`.
- a PRIVATE DRILL CATALOG (admin/app-catalog-drill), so that broken, dummy, cross-repo and
  engine-major edges can be measured without the live catalog ever carrying one. Positive
  control quoted, and two negative controls: the live catalog's main and both real boxes'
  caches unchanged.
- a throwaway image store on the scratch guest, which is what makes an UNATTENDED HOLD
  measurable at all: an edge that PASSES the within-a-major test and still fails.
  CompareImageRefs was proven to order host:port/ references by RUNNING it (4 positive cases
  + 1 negative control), not by reading it.

Phase 1: real within-a-major upstream edges walked on guest 9202 through the product's own
guarded Update, each app seeded and read back through its OWN front door (R-156), with a
per-edge verdict record in 09's shape. `inconclusive` is never collapsed into `failed`.

TWO INSTRUMENT FIXES, both in this repo's own evidence code:
- 00-api-recipe.md said the app page is /app/<n>; it is /apps/<n>, and every call it described
  404s. Corrected, with the session-expiry note that cost the same time.
- unattended-caller.py's follow() read update_phase/updating off the API ENVELOPE, so both were
  always None and EVERY followed update ran to its 900 s timeout and was then recorded
  `timeout` and never-press-again. Fixed before B1 relied on it. R-623.

No controller, agent or hub code was written. The live catalog carries no broken reference.

Gates: repo_gates.py --fast — all 15 OK, exit 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-21 21:17:46 +02:00

5.0 KiB

Driving the guest-9202 controller API from DooPlex — the working recipe (2026-09-21)

Controller v0.260.0 in LXC guest 9202 demo-hp-scratch on host demo-hp.

The one surprise that saves the most time

Guest 9202 is directly reachable from DooPlex on the home LAN at 192.168.0.114. You do NOT have to ssh demo-hp + pct exec to drive the API — that was only needed for docker/pct. Curling straight from DooPlex is what makes a 200 ms poll loop possible at all.

Still mandatory: the Host: felhom.enkisfelhom.hu header (without it every route 404s with the public page) and -k (self-signed cert on traefik). Port 443. 127.0.0.1:8080 inside the guest is NOT open — traefik is the only door.

0. The password, file→file, never through stdout

python3 /mnt/5_hdd/felhom.eu/git/felhom.eu/scripts/read_credential.py PASSWORD /tmp/.ctlpw
chmod 600 /tmp/.ctlpw          # expect 13 bytes; 15 means it is wearing its quotes

1. Log in and scrape the session CSRF (both are needed for any POST)

curl's cookie jar drops felhom_session, so dump the headers and grep it out by hand. The CSRF meta tag's closing quote must be stripped or you get a 65-char token that silently mismatches — check the length is exactly 64.

S=/tmp/ctl                      # any scratch dir
mkdir -p $S
B="https://192.168.0.114"
H='Host: felhom.enkisfelhom.hu'
PW=$(cat /tmp/.ctlpw)

curl -sk -D $S/hdr.txt -o /dev/null -H "$H" -X POST --data-urlencode "password=$PW" "$B/login"
head -1 $S/hdr.txt                                    # expect: HTTP/2 302
grep -oiE 'felhom_session=[A-Za-z0-9._-]+' $S/hdr.txt | head -1 > $S/sess.txt   # ~79 chars

curl -sk -L -H "$H" -H "Cookie: $(cat $S/sess.txt)" "$B/" -o $S/home.html
grep -oE '<meta name="csrf-token" content="[^"]+"' $S/home.html | head -1 \
  | sed 's/.*content="//;s/"$//' > $S/csrf.txt
echo "csrf len: $(wc -c < $S/csrf.txt)"               # expect 65 = 64 + newline

2. The call helper — c.sh GET /api/stacks / c.sh POST /api/sync '{...}'

cat > $S/c.sh <<'EOF'
#!/bin/bash
S=/tmp/ctl
B="https://192.168.0.114"
H='Host: felhom.enkisfelhom.hu'
M=$1; P=$2; D=$3
SESS=$(cat $S/sess.txt); CT=$(cat $S/csrf.txt)
if [ "$M" = GET ]; then
  curl -sk -H "$H" -H "Cookie: $SESS" "$B$P"
else
  curl -sk -H "$H" -H "Cookie: $SESS" -H "X-CSRF-Token: $CT" \
       -H "Content-Type: application/json" -X "$M" ${D:+--data "$D"} "$B$P"
fi
EOF
chmod +x $S/c.sh

Worked endpoints (all verified today):

call meaning
c.sh GET /api/stacks every stack; app_config.pinned_images, app_config.installed_images, template_images, catalog_images, updating, update_phase
c.sh GET /api/stacks/vikunja one stack, same shape — this is the 200 ms poll target
c.sh POST /api/stacks/<n>/deploy '{"values":{"DOMAIN":"enkisfelhom.hu","SUBDOMAIN":"tasks"}}' real deploy path, answers 202
c.sh POST /api/stacks/<n>/update the Update button
c.sh POST /api/sync pull the catalog git clone
c.sh POST /api/stacks/rescan always run this after a sync before reading any badge (R-607)
c.sh GET /api/stacks/<n>/logs?lines=200 app container log

pinned_images and installed_images live under app_config, not at the top level — that cost a few minutes.

3. Reading the customer's app page, both languages

The route is /apps/<name>, not /app/<name>. This section said /app/ until 2026-09-21 and every call it described 404s. / redirects to /launcher, and the app links live on /stacks.

curl -sk -H "$H" -H "Cookie: $(cat $S/sess.txt)" "$B/apps/vikunja"            # Hungarian
curl -sk -H "$H" -H "Cookie: $(cat $S/sess.txt)" "$B/apps/vikunja?lang=en"    # English

The session dies whenever the controller restarts — the store is in memory — so a long run must be able to log in again mid-flight rather than assuming one login lasts the night.

4. Shell into the guest (for docker / pct only)

ssh -o StrictHostKeyChecking=accept-new demo-hp "pct exec 9202 -- bash -c '<cmd>'" 2>/dev/null

2>/dev/null drops the perl locale warnings. For anything with awkward quoting, pipe a script:

cat <<'EOF' | ssh -o StrictHostKeyChecking=accept-new demo-hp \
  'cat > /tmp/c.sh; pct push 9202 /tmp/c.sh /tmp/c.sh >/dev/null 2>&1; pct exec 9202 -- bash /tmp/c.sh' 2>/dev/null
docker ps --format '{{.Names}}\t{{.Image}}'
EOF

5. Paths inside the running guest

  • controller data dir (journal, catalog cache): /var/lib/docker/volumes/felhom-controller-data/_data/data/
    • update-journal.json — present only while an update is in flight
    • catalog-cache/ — a git clone; git -C … log --oneline -1 tells you what the box actually has
  • stacks: /opt/docker/stacks/<app>/docker-compose.yml (the LIVE rendered file) and app.yaml

6. App front doors on 9202 (Host header per app, same IP)

tasks. vikunja · status. uptime-kuma · wishes. wishlist · dashboard. glance — all …enkisfelhom.hu against https://192.168.0.114.