docs: a negative result is usually your own filter (skill + AGENTS.md)
Three false negatives in one session, all self-inflicted, all convincing because the command "succeeded": a `| head -20` proved an SSH peer absent that sits at line 454 of a ~500-line config; `ssh mac 'docker ps'` proved the host had no Docker, when the non-interactive PATH simply lacks /usr/local/bin; and `grep 'ssh '` proved no ControlMaster was running, when those processes rename themselves to `ssh: <path> [mux]`. Same root cause each time, so it goes in the skill rather than in a commit message: a positive result carries its own evidence, absence has to be earned. The skill (rootfs/, symlinked into ~/.agents/skills) is BAKED, so this is an image change and is logged in CHANGELOG Unreleased accordingly. Its §3 also now records that a live ControlMaster socket makes later commands authenticate not at all -- after editing a peer's authorized_keys, "it still works" proves nothing; prove it with -o ControlPath=none, or the breakage waits for a future session that has no memory of the edit. AGENTS.md: corrected a stale CI claim while placing the pointer. It said a tag push produces two runs including lint; lint.yml has since been scoped to branches: ['**'], which excludes tag refs, and refs/tags/v1.8.4 duly produced run 571 (publish) and nothing else. Kept the head_sha + workflow path filter advice, which is cheap and guards against a future v*-triggered workflow. Added a short section on verifying this repo from inside a container, including that docker-compose.yml here is a TEMPLATE pinning :latest while a real host runs its own per-machine file -- recreating from the repo copy can silently move a host off :latest-studio. Placement note: AGENTS.md is only auto-read when the cwd is this repo, so the durable rule lives in the skill, which loads by description match in any pi-devbox session.
This commit is contained in:
@@ -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`.
|
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 +
|
5. Watch CI: smoke job builds amd64 only and asserts size + extensions +
|
||||||
pi version + new-base-tooling presence. Variant build is multi-arch
|
pi version + new-base-tooling presence. Variant build is multi-arch
|
||||||
(amd64 + arm64) only after smoke passes. **A tag push produces two runs, not
|
(amd64 + arm64) only after smoke passes. A tag push fires **only**
|
||||||
one** — `lint.yml` fires on every push (including tag refs) and
|
`docker-publish.yml` — `lint.yml` is scoped to `branches: ['**']`, which
|
||||||
`docker-publish.yml` fires on `v*` tags. Watch the **publish** run; see
|
excludes tag refs on purpose (the tagged tree was already linted when the
|
||||||
*Gitea API access* below for how to find it without picking lint by mistake.
|
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
|
6. Verify the Hub tags appear (latest + vX.Y.Z, the `-studio` pair, plus
|
||||||
base-latest if the base was rebuilt this run).
|
base-latest if the base was rebuilt this run).
|
||||||
7. **Revoke any short-lived Gitea PAT** used during the release at
|
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) —
|
`GITEA_ACCESS_TOKEN` env var instead (see *Gitea API access* below) —
|
||||||
its lifecycle is managed host-side, nothing to revoke.
|
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 <container> --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 API access (env token)
|
||||||
|
|
||||||
`GITEA_ACCESS_TOKEN` + `GITEA_HOST` are passed into the container from the
|
`GITEA_ACCESS_TOKEN` + `GITEA_HOST` are passed into the container from the
|
||||||
|
|||||||
@@ -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: <controlpath> [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
|
## v1.8.4 — 2026-08-22
|
||||||
|
|
||||||
Patch release, and the one that ends an eight-week bug: **the baked
|
Patch release, and the one that ends an eight-week bug: **the baked
|
||||||
|
|||||||
@@ -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
|
If a command "works in my terminal but not when the agent runs it," this alias
|
||||||
gap is the first thing to suspect.
|
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: <controlpath> [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
|
**`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
|
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
|
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
|
CGNAT per-destination flow caps on residential ISPs. If `~/.ssh/config` pins
|
||||||
a `ControlPath` under the read-only `~/.ssh`, override with
|
a `ControlPath` under the read-only `~/.ssh`, override with
|
||||||
`-o ControlPath=none` (or use the sidecar, which already redirects it).
|
`-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/<user>@<host>: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 <host> # 'Master running (pid=N)'
|
||||||
|
ssh -F "$HOME/.ssh-local/config" -o ControlPath=none -o ControlMaster=no \
|
||||||
|
-o BatchMode=yes <host> 'echo COLD AUTH OK'
|
||||||
|
```
|
||||||
|
|
||||||
|
To attribute a socket rather than guess whose it is: `ps -p <pid> -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 <host>`** rewires pi's own read/write/edit/bash tools to run on a
|
- **`pi --ssh <host>`** 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`
|
remote host; it has its own writable-socket fallback. See the `pi-extensions`
|
||||||
skill for that path.
|
skill for that path.
|
||||||
@@ -257,6 +304,10 @@ hardcode. Details are in the `mempalace` skill.
|
|||||||
- [ ] Writing durable output? → `/workspace`, not the ephemeral layer.
|
- [ ] Writing durable output? → `/workspace`, not the ephemeral layer.
|
||||||
- [ ] Using `dssh`/`dscp`/`ll` in the bash tool? → spell out the real command.
|
- [ ] Using `dssh`/`dscp`/`ll` in the bash tool? → spell out the real command.
|
||||||
- [ ] Assuming a hostname / domain / nameserver / host OS? → stop, detect it.
|
- [ ] 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).
|
- [ ] "Resolves but won't connect"? → check route *and* DNS (§3 + §4).
|
||||||
- [ ] `apt`/toolchain install? → tell the user it's ephemeral unless imaged.
|
- [ ] `apt`/toolchain install? → tell the user it's ephemeral unless imaged.
|
||||||
- [ ] Editing a skill? → `readlink -f ~/.agents/skills/<name>` first (§1).
|
- [ ] Editing a skill? → `readlink -f ~/.agents/skills/<name>` first (§1).
|
||||||
|
|||||||
Reference in New Issue
Block a user