From 7f7af9292cd969c78d8ac21e2b986fc4edefb678 Mon Sep 17 00:00:00 2001 From: makseq Date: Tue, 11 Aug 2026 13:10:03 +0300 Subject: [PATCH] docs: the setup block is safe to paste again, and this copy says so MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The block in `Starting the agent` was still the version written as though it ran once, and it is in fact run again after every re-registration, every agent build and every mistake. The second paste ended in `docker: Error response from daemon: Conflict. The container name "/lspo-agent" is already in use` — an error message about a machine that was working perfectly, and the reader of this repository is exactly the person who hit it. The orchestrator fixed the generator that prints the block (orchestrator #271); this brings our copy of it in line. The block now fetches the image, looks for a container left by an earlier paste, stops and removes it, and starts a fresh one. On a machine that never ran it, the two middle lines find nothing and say nothing. The three lines are chained with `&&` and that is a guarantee, not a layout choice: **a failed fetch takes nothing away**. The shell this is pasted into is an interactive one with no `set -e`, where a failed pull stops nothing on its own — so as separate lines the same block would go on to remove a working agent and then start a stale cached image, or none at all. That is measured rather than reasoned about: on a real daemon, with an unreachable registry, the chained block left the existing agent running and the unchained one destroyed a healthy agent and left the machine with none. Hence the RECOMMENDATION not to tidy the lines apart when quoting them into a runbook, and not to drop the single quotes around the filter, where `?` is otherwise a shell glob. Two smaller corrections in the same section, both pre-existing drift from what the product prints: * `LSPO_AGENT_NAME` was listed before `LSPO_AGENT_POOL` and `LSPO_AGENT_REGISTRATION_TOKEN`, and `-e LSPO_AGENT_MAX_CONCURRENT_JOBS=1` was missing entirely. * "`docker run` pulls it for you; `docker pull …` fetches it on its own if you would rather do that first" is no longer true of a block that fetches first. It now says what the block does, and keeps the standalone pull for the job it is actually good for: establishing that a machine can reach the registry at all. The fenced block is byte-identical to `noderegistry.services.docker_run_command`'s output at orchestrator `3b622553` — checked mechanically, not by eye: the block is extracted from this file and compared with the generator's return value called with this section's own placeholders (`self-hosted`, `https://orchestrator.example.com`, no token, which is what makes the token line read `PASTE_THE_POOL_TOKEN_HERE`). Same SHA-256, all 19 lines, whitespace included. `python -m pytest`: 128 passed, 1 skipped in 134s — the skip is the verbatim citation check, which needs an orchestrator checkout. Run with one (`LSPO_ORCHESTRATOR_SRC` pointed at `3b622553`) it passes too. Co-authored-by: Claude Fable 5 --- docs/OPERATIONS.md | 47 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 4c557ea..a75ca17 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -140,9 +140,12 @@ streams its logs back and reports the result. It listens on no port; every conne outbound. **BEHAVIOUR.** The agent image is published at **`ghcr.io/humansignal/lspo-agent:latest`**, -and the package is **public**: an anonymous pull works and no `docker login` is needed. -`docker run` pulls it for you; `docker pull ghcr.io/humansignal/lspo-agent:latest` fetches -it on its own if you would rather do that first. +and the package is **public**: an anonymous pull works and no `docker login` is needed. The +block below fetches the image itself, before it starts or stops anything, so there is nothing +to build and nothing to fetch by hand first. A standalone `docker logout ghcr.io && docker +pull ghcr.io/humansignal/lspo-agent:latest` is worth keeping for a different job: it is how +you establish that a machine can reach the registry at all, anonymously, which is the one +question that separates "this host has no route to ghcr.io" from anything about your node. Both the Connect reply and the setup command print the start line already filled in — prefer either of those, because they carry the real pool name and, when that registration @@ -151,6 +154,10 @@ minted one, the real token. The general form: ```bash mkdir -p "$HOME/lspo-agent" && chmod 700 "$HOME/lspo-agent" +# Safe to re-run: it fetches a newer agent image and replaces the running agent. A failed fetch changes nothing. +docker pull -q ghcr.io/humansignal/lspo-agent:latest && +c=$(docker ps -aq -f 'name=^/?lspo-agent$') && +if [ -n "$c" ]; then docker stop -t 20 "$c" && docker rm "$c"; fi && docker run -d --name lspo-agent \ --user "$(id -u):$(id -g)" \ -v /var/run/docker.sock:/var/run/docker.sock \ @@ -158,9 +165,10 @@ docker run -d --name lspo-agent \ -v "$HOME/lspo-agent:$HOME/lspo-agent" \ -e LSPO_AGENT_WORKDIR="$HOME/lspo-agent/state" \ -e LSPO_AGENT_API_URL=https://orchestrator.example.com \ - -e LSPO_AGENT_NAME=$(hostname) \ -e LSPO_AGENT_POOL=self-hosted \ -e LSPO_AGENT_REGISTRATION_TOKEN=PASTE_THE_POOL_TOKEN_HERE \ + -e LSPO_AGENT_NAME=$(hostname) \ + -e LSPO_AGENT_MAX_CONCURRENT_JOBS=1 \ --stop-timeout 300 \ ghcr.io/humansignal/lspo-agent:latest ``` @@ -169,6 +177,29 @@ Three of those values are placeholders and the printed command has them filled i orchestrator's address, the pool name, and the token — see the registration section above for when the reply carries a token and when it does not. +**BEHAVIOUR.** Paste it again whenever you like. That is the ordinary thing to do — after +re-registering a node, after a new agent build, after any mistake — and the block is written +for it: it fetches the image, stops and removes the agent that is already running, and starts +a fresh one. On a machine that has never run it, the two middle lines find nothing and say +nothing. Before they existed, the second paste ended in `docker: Error response from daemon: +Conflict. The container name "/lspo-agent" is already in use`, which is an error message about +a machine that was working perfectly. + +**BEHAVIOUR.** If the fetch fails, nothing is taken away. No network, an unreachable registry, +a package whose visibility was changed back — in every one of those you see docker's own pull +error and keep the agent you already have, running, untouched. That holds at a shell prompt as +well as inside a script, and it is the `&&` at the end of those lines that makes it hold: an +interactive shell has no `set -e`, so without the chain a failed pull would stop nothing, and +the block would go on to remove a working agent and then start a stale cached image or none at +all. The same conditionality covers the later steps: a stop that did not work removes nothing, +a removal that did not work starts nothing. + +**RECOMMENDATION.** Quote those lines into your own runbook as they are — one chain, and the +filter still in its single quotes. Splitting the chain into separate lines reads the same and +looks tidier, and it is the version that destroys a healthy agent the first time a registry is +unreachable; dropping the quotes leaves `?` as a shell glob, so a matching filename in the +working directory is handed to docker in place of the filter. + **BEHAVIOUR.** The first line is a prerequisite rather than decoration, and skipping it does not fail where the mistake is. `docker run` needs the mount source to exist, and when it does not, the daemon creates it — **as root**. The agent then cannot create its own state directory @@ -208,10 +239,10 @@ the orchestrator prints with the block, word for word: > links in that path. Satisfy yourself of that before you start, and prefer a machine you do > not share. -**BEHAVIOUR, for all four bullets below.** Four parts of that command are load-bearing, and -each one fails in its own way when it is wrong (`agent/README.md`, "Run it"). None of them -is a duty on your node — they are the operator's, and they are here because your node is -what visibly breaks: +**BEHAVIOUR, for all four bullets below.** Four parts of the `docker run` line itself are +load-bearing — the `&&` chain above it is a fifth — and each one fails in its own way when it +is wrong (`agent/README.md`, "Run it"). None of them is a duty on your node — they are the +operator's, and they are here because your node is what visibly breaks: * **`--group-add`** with the host's docker group id. The agent image runs as a non-root user and has no access to the docker socket without it. The agent refuses to start