diff --git a/AGENTS.md b/AGENTS.md index 24f18f2..a5bb7ae 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,10 +76,15 @@ re-brand of opencode-devbox's `pi-only` variant. 4. Push tag: `git tag vX.Y.Z && git push origin vX.Y.Z`. 5. Watch CI: smoke job builds amd64 only and asserts size + extensions + pi version + new-base-tooling presence. Variant build is multi-arch - (amd64 + arm64) only after smoke passes. **A tag push produces two runs, not - one** — `lint.yml` fires on every push (including tag refs) and - `docker-publish.yml` fires on `v*` tags. Watch the **publish** run; see - *Gitea API access* below for how to find it without picking lint by mistake. + (amd64 + arm64) only after smoke passes. A tag push fires **only** + `docker-publish.yml` — `lint.yml` is scoped to `branches: ['**']`, which + excludes tag refs on purpose (the tagged tree was already linted when the + commit hit `main`, and a fast lint run sorting above the slow publish run + made releases look finished before anything shipped). Verified on v1.8.4: + `refs/tags/v1.8.4` produced run 571 (publish) and nothing else. Still filter + discovery on `head_sha` **and** the workflow `path` — see *Gitea API access* + below — because that guard costs nothing and a future workflow added on `v*` + would silently reintroduce the ambiguity. 6. Verify the Hub tags appear (latest + vX.Y.Z, the `-studio` pair, plus base-latest if the base was rebuilt this run). 7. **Revoke any short-lived Gitea PAT** used during the release at @@ -87,6 +92,34 @@ re-brand of opencode-devbox's `pi-only` variant. `GITEA_ACCESS_TOKEN` env var instead (see *Gitea API access* below) — its lifecycle is managed host-side, nothing to revoke. +## Verifying this repo's reality from inside a container + +Most work on this repo happens **inside** a pi-devbox container, inspecting a +host or a peer over SSH. That setup manufactures convincing false negatives, so +when you are about to report that something is **absent, unreachable, or not +running**, suspect your own command first. Recurring instances: + +- **`docker` is not on the host's non-interactive SSH `PATH`.** `ssh mac 'docker + ps'` says *command not found* on a host that plainly runs Docker; use + `/usr/local/bin/docker` (or `command -v docker` first). Every step in the + *Release-day checklist* that inspects a running container hits this. +- **Don't `| head -N` a search whose answer you don't already know.** The host's + `~/.ssh/config` is ~500 lines; a `head -20` "proved" a peer absent that was + defined at line 454. +- **The deployment compose file is not this repo's.** `docker-compose.yml` here + is a template pinning `:latest`; a real host runs its own per-machine file + (find it with `docker inspect --format '{{ index .Config.Labels + "com.docker.compose.project.config_files" }}'`). Recreating from the repo copy + can silently move a host off `:latest-studio` onto `:latest`. +- **A live SSH ControlMaster hides remote auth changes** — after editing a + peer's `authorized_keys`, prove access with `-o ControlPath=none -o + ControlMaster=no`, or the breakage surfaces in a later session instead. + +Depth and further mechanisms: the repo-authored `pi-devbox-environment` skill +(`rootfs/usr/local/share/pi-devbox/skills/pi-devbox-environment/SKILL.md`) §2 +and §3 — that file is the one an agent actually loads mid-session, whereas this +`AGENTS.md` is only auto-read when the cwd *is* this repo. + ## Gitea API access (env token) `GITEA_ACCESS_TOKEN` + `GITEA_HOST` are passed into the container from the diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f63203..302e10d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,42 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`). --- +## Unreleased + +Docs only so far, but one of the two files ships **inside** the image. + +### Changed + +- **`pi-devbox-environment` skill — new §2 subsection "A negative result is + usually your own filter", plus ControlMaster masking in §3.** This is baked + (`rootfs/usr/local/share/pi-devbox/skills/`, symlinked to + `~/.agents/skills/`), so it is an image-behaviour change even though no + package moved. Motivated by three false negatives an agent produced in a + single session, each from its own filter rather than from the world: a + `| head -20` "proved" an SSH peer absent that was defined at **line 454** of a + ~500-line config; `ssh mac 'docker ps'` "proved" the host had no Docker, when + the non-interactive SSH `PATH` simply lacks `/usr/local/bin`; and a `grep 'ssh + '` "proved" no ControlMaster was running, when master processes **rename + themselves** to `ssh: [mux]`. The rule now stated: a positive + result carries its own evidence, absence has to be *earned*. §3 additionally + documents that a live master socket makes later commands authenticate **not at + all**, so "it still works" proves nothing after editing a peer's + `authorized_keys` — verify with `-o ControlPath=none -o ControlMaster=no`, or + the breakage surfaces in a future session with no memory of the edit. +- **`AGENTS.md`: a stale CI claim corrected.** It said "a tag push produces two + runs, not one — `lint.yml` fires on every push (including tag refs)". That + stopped being true when lint was scoped to `branches: ['**']`, which excludes + tag refs by design; `refs/tags/v1.8.4` produced run 571 (publish) and nothing + else. The `head_sha` + workflow-`path` filter advice stays, because it costs + nothing and any future `v*`-triggered workflow would reintroduce the + ambiguity. Also adds a short "Verifying this repo's reality from inside a + container" section, including the trap that **this repo's `docker-compose.yml` + is a template pinning `:latest`** while a real host runs its own per-machine + file — so recreating from the repo copy can silently move a host off + `:latest-studio`. + +--- + ## v1.8.4 — 2026-08-22 Patch release, and the one that ends an eight-week bug: **the baked diff --git a/rootfs/usr/local/share/pi-devbox/skills/pi-devbox-environment/SKILL.md b/rootfs/usr/local/share/pi-devbox/skills/pi-devbox-environment/SKILL.md index b433541..c7e79ff 100644 --- a/rootfs/usr/local/share/pi-devbox/skills/pi-devbox-environment/SKILL.md +++ b/rootfs/usr/local/share/pi-devbox/skills/pi-devbox-environment/SKILL.md @@ -130,6 +130,36 @@ are "command not found" there — you must spell out the underlying command. If a command "works in my terminal but not when the agent runs it," this alias gap is the first thing to suspect. +### A negative result is usually your own filter + +**When you are about to report that something is absent, unreachable, or not +running, the filter you wrote is the prime suspect — not the thing.** This +environment produces false negatives cheaply, and they are convincing because +the command "succeeded". Three real instances from one session, all wrong, all +mine: + +| Claim I made | Why it was false | +|---|---| +| "`tor-ms22` is not in the SSH config" | `grep … \| head -20` — the entry was at **line 454**. `~/.ssh/config` here is ~500 lines. | +| "the Docker host has no `docker`" | non-interactive SSH `PATH` lacks `/usr/local/bin` (§2, §3). It was at `/usr/local/bin/docker`. | +| "no ControlMaster is running" | pattern `ssh ` (trailing space) cannot match a master: those processes **rename themselves** to `ssh: [mux]`. | + +Habits that would have caught all three: + +```sh +# don't cap the output of a search whose answer you don't already know +grep -n -i -A6 'tor-ms22' ~/.ssh/config # not | head -20 + +# on the host, resolve the binary instead of trusting PATH +ssh -F "$HOME/.ssh-local/config" mac 'command -v docker || ls /usr/local/bin/docker' + +# match a process's ACTUAL argv, not the name you imagine +ps -eo pid,etime,args | grep -Ei 'mux|mosh|ssh' +``` + +A positive result needs no such scepticism — it carries its own evidence. Only +absence has to be *earned*, so spend the extra command there. + **`dscp`/`scp` with accented filenames on a macOS host.** macOS stores filenames in Unicode **NFD** (decomposed — e.g. `ä` is `a` + combining U+0308), while the string you type or paste is usually **NFC** (precomposed `ä`, U+00E4). The bytes @@ -175,6 +205,23 @@ Two related mechanisms (don't reinvent them): CGNAT per-destination flow caps on residential ISPs. If `~/.ssh/config` pins a `ControlPath` under the read-only `~/.ssh`, override with `-o ControlPath=none` (or use the sidecar, which already redirects it). +- **A live master socket MASKS auth and config changes on the far end.** Once + `~/.ssh-local/cm/@:22` exists, later commands ride it and + authenticate **not at all** — so after editing remote `authorized_keys`, + `sshd_config`, host keys, or firewall rules, "it still works" proves nothing. + A corrupted `authorized_keys` then bites on the next *cold* connect, likely in + a future session with no memory of the edit. Prove it immediately instead: + + ```sh + ssh -F "$HOME/.ssh-local/config" -O check # 'Master running (pid=N)' + ssh -F "$HOME/.ssh-local/config" -o ControlPath=none -o ControlMaster=no \ + -o BatchMode=yes 'echo COLD AUTH OK' + ``` + + To attribute a socket rather than guess whose it is: `ps -p -o + pid,ppid,lstart,etime,args`. A `mosh` the *user* started on the host + bootstraps with the **host's** `~/.ssh/cm/` and is invisible from in here; + only a mosh started *inside* the container shares `~/.ssh-local/cm/`. - **`pi --ssh `** rewires pi's own read/write/edit/bash tools to run on a remote host; it has its own writable-socket fallback. See the `pi-extensions` skill for that path. @@ -257,6 +304,10 @@ hardcode. Details are in the `mempalace` skill. - [ ] Writing durable output? → `/workspace`, not the ephemeral layer. - [ ] Using `dssh`/`dscp`/`ll` in the bash tool? → spell out the real command. - [ ] Assuming a hostname / domain / nameserver / host OS? → stop, detect it. +- [ ] About to report something **absent / unreachable / not running**? → re-run + without your own `head`/pattern/`PATH` assumptions first (§2). +- [ ] Changed remote `authorized_keys` / `sshd_config`? → prove it with a **cold** + connect; a live master socket hides breakage (§3). - [ ] "Resolves but won't connect"? → check route *and* DNS (§3 + §4). - [ ] `apt`/toolchain install? → tell the user it's ephemeral unless imaged. - [ ] Editing a skill? → `readlink -f ~/.agents/skills/` first (§1).