docs: correct the "pi-studio has no bind flag" claim everywhere it was baked
Lint / hadolint (push) Successful in 10s
Lint / skill-floor (push) Successful in 14s
Lint / doc-drift (push) Failing after 18s
Lint / actionlint (push) Successful in 31s

1e4c238 fixed README; the same claim lived in three more places, all wrong
since pi-studio 0.9.60 added /studio --listen-all:

- rootfs/usr/local/bin/studio-expose header (and so its --help; the sed range
  grows 2,31p -> 2,35p so --help still ends at "runs it backgrounded.")
- pi-devbox-environment skill section 7: --listen-all as a third path, plus the
  0.10 whole-URL / 409 / "link looks damaged" notes, the published-port rule,
  --stop before --port, and the on-host curl 403-vs-empty-reply diagnostic
- Dockerfile.variant STUDIO_PORT comment

Bridge and --listen-all on one port: whichever starts second fails, measured
both ways on tor-ms22 (v1.11.2): Studio "listen EADDRINUSE ... 0.0.0.0:8765"
with the bridge up; studio-expose rc 1 on socat "bind(...): Address already in
use" with a 0.0.0.0 listener up. README's warning now says both directions.

Cost: studio-expose and the skill are in rootfs/, which base_tag hashes, so the
next release rebuilds the base (recomputed with the CI recipe: base-6f3b20266de1
at 1e4c238 -> base-26d425cea6c2 now). Noted in the CHANGELOG Unreleased entry.
No pin changes. lint-shell, dockerfile-directives and base-hash guards pass;
check-doc-drift is red only for the unrelated pi-extensions 750b2a8 -> 0d8e417.
This commit is contained in:
2026-10-14 19:27:42 +02:00
parent 1e4c238a83
commit a8f5b35d2a
5 changed files with 56 additions and 25 deletions
+16 -6
View File
@@ -13,18 +13,28 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
## Unreleased ## Unreleased
Docs only: this entry changes no pin. Docs and comments only: this entry changes no pin. Two of the files are
baked into the base image (`rootfs/usr/local/bin/studio-expose` and the
`pi-devbox-environment` skill), and `base_tag` hashes all of `rootfs/`, so
**the next release rebuilds the base** rather than reusing
`base-6f3b20266de1`.
### Fixed ### Fixed
- **README "Using pi-studio" said pi-studio has no bind flag.** It has had - **README "Using pi-studio" said pi-studio has no bind flag.** It has had
`/studio --listen-all` (binds `0.0.0.0`) since pi-studio 0.9.60, so the `/studio --listen-all` (binds `0.0.0.0`) since pi-studio 0.9.60, so the
section now lists it as path **C**, next to host networking (A) and the section now lists it as path **C**, next to host networking (A) and the
`studio-expose` bridge (B). It also warns not to combine B and C on one port. `studio-expose` bridge (B). It also warns not to combine B and C on one
With `STUDIO_EXPOSE=1` the bridge holds the egress IP on `$STUDIO_PORT` port, because whichever starts second fails. With `STUDIO_EXPOSE=1`,
from boot, and `/studio --listen-all --port 8765` then fails with `/studio --listen-all --port 8765` fails with
`listen EADDRINUSE: address already in use 0.0.0.0:8765` (measured on `listen EADDRINUSE: address already in use 0.0.0.0:8765`. In the other
tor-ms22, v1.11.2). order, `studio-expose` exits 1 on socat's `bind(…): Address already in
use` (both measured on tor-ms22, v1.11.2). The same stale "no bind
flag" claim is corrected in the three other places that carried it:
the `studio-expose` header (and so its `--help`, whose `sed` range
grows from `2,31p` to `2,35p`), §7 of the baked `pi-devbox-environment`
skill (which also gains the whole-URL, published-port and `curl` notes
below), and the `STUDIO_PORT` comment in `Dockerfile.variant`.
- **The remote recipe described a pi-studio 0.9 URL.** Since 0.10 the printed - **The remote recipe described a pi-studio 0.9 URL.** Since 0.10 the printed
URL carries `studioTabState` and `hostedWorkspace` as well as the token, and URL carries `studioTabState` and `hostedWorkspace` as well as the token, and
a token-only URL is answered `409`. The recipe now says to open the whole a token-only URL is answered `409`. The recipe now says to open the whole
+5 -4
View File
@@ -676,10 +676,11 @@ RUN if [ "${INSTALL_STUDIO}" = "true" ]; then \
# STUDIO_PORT: advisory default consumed by docker-compose port publishing # STUDIO_PORT: advisory default consumed by docker-compose port publishing
# and the recommended `/studio --no-browser --port "$STUDIO_PORT"` launch. # and the recommended `/studio --no-browser --port "$STUDIO_PORT"` launch.
# Harmless in the non-studio variant. NOTE: pi-studio hard-binds the server # Harmless in the non-studio variant. NOTE: by default pi-studio binds the
# to 127.0.0.1 inside the container (index.ts: .listen(port,"127.0.0.1")), # server to 127.0.0.1 inside the container, so reaching it from a browser
# so reaching it from a browser needs a loopback bridge or host networking — # needs host networking, the studio-expose loopback bridge, or the opt-in
# see the "Using pi-studio" section in README.md. # `/studio --listen-all` (pi-studio >= 0.9.60; never together with the
# bridge on one port) — see the "Using pi-studio" section in README.md.
ENV STUDIO_PORT=8765 ENV STUDIO_PORT=8765
# ── Optional: Go toolchain ─────────────────────────────────────────── # ── Optional: Go toolchain ───────────────────────────────────────────
+5 -4
View File
@@ -261,10 +261,11 @@ publish. Keep the publish on `127.0.0.1:` as in B: anyone who can reach the
port and has the full URL controls Studio, including running prompts and port and has the full URL controls Studio, including running prompts and
reading or writing files, and there is no TLS. reading or writing files, and there is no TLS.
> **Use B or C on a port, not both.** With `STUDIO_EXPOSE=1` the bridge holds > **Use B or C on a port, not both.** Whichever starts second fails with
> the container's egress IP on `$STUDIO_PORT` from boot, and `--listen-all` on > `EADDRINUSE`: `0.0.0.0` includes the egress IP the bridge binds. With
> that same port fails with `EADDRINUSE` (`0.0.0.0` includes that address). > `STUDIO_EXPOSE=1` the bridge holds `$STUDIO_PORT` from boot, so
> If the bridge is running, start Studio *without* `--listen-all`. > `/studio --listen-all --port 8765` fails. If the bridge is running, start
> Studio *without* `--listen-all`.
### Remote host (SSH / mosh) ### Remote host (SSH / mosh)
+11 -7
View File
@@ -3,13 +3,17 @@
# through a published Docker port. # through a published Docker port.
# #
# WHY THIS EXISTS # WHY THIS EXISTS
# pi-studio hard-binds its HTTP/WebSocket server to 127.0.0.1 inside the # By default pi-studio binds its HTTP/WebSocket server to 127.0.0.1 inside
# container (index.ts: `.listen(port, "127.0.0.1")`) and there is no # the container. A plain `docker run -p 8765:8765` forwards to the
# --host / bind flag. A plain `docker run -p 8765:8765` forwards to the
# container's EXTERNAL interface (eth0), not its loopback, so it cannot # container's EXTERNAL interface (eth0), not its loopback, so it cannot
# reach Studio. This helper runs a socat TCP relay that listens on the # reach a default Studio. This helper runs a socat TCP relay that listens
# container's egress IP and forwards to 127.0.0.1:<port>, so a published # on the container's egress IP and forwards to 127.0.0.1:<port>, so a
# port (and an `ssh -L` tunnel from your laptop) can reach Studio. # published port (and an `ssh -L` tunnel from your laptop) can reach Studio.
#
# The alternative (pi-studio >= 0.9.60) is `/studio --listen-all`, which
# binds 0.0.0.0 and needs no bridge. Use one or the other on a port, never
# both: whichever starts second fails with EADDRINUSE (0.0.0.0 includes
# the egress IP this relay binds).
# #
# SECURITY # SECURITY
# This intentionally exposes Studio beyond loopback — anything that can # This intentionally exposes Studio beyond loopback — anything that can
@@ -34,7 +38,7 @@ set -euo pipefail
PORT="${1:-${STUDIO_PORT:-8765}}" PORT="${1:-${STUDIO_PORT:-8765}}"
if [ "$PORT" = "--help" ] || [ "$PORT" = "-h" ]; then if [ "$PORT" = "--help" ] || [ "$PORT" = "-h" ]; then
sed -n '2,31p' "$0" | sed 's/^# \{0,1\}//' sed -n '2,35p' "$0" | sed 's/^# \{0,1\}//'
exit 0 exit 0
fi fi
@@ -356,8 +356,8 @@ window: 0". Leave the indexing alone in this environment.
## 7. pi-studio reachability (only in the `-studio` variant) ## 7. pi-studio reachability (only in the `-studio` variant)
Present only if `/opt/pi-studio` exists / the `studio_*` tools are in your tool Present only if `/opt/pi-studio` exists / the `studio_*` tools are in your tool
list. pi-studio **binds to `127.0.0.1` inside the container** with no host-bind list. By default pi-studio **binds to `127.0.0.1` inside the container**, so a
flag, so a plain `docker -p` publish can't reach it. Two supported paths: plain `docker -p` publish can't reach it. Three supported paths:
- **Host networking** (`network_mode: host`): container loopback == host - **Host networking** (`network_mode: host`): container loopback == host
loopback; open the tokenized URL on the host. (Changes loopback; open the tokenized URL on the host. (Changes
@@ -365,10 +365,25 @@ flag, so a plain `docker -p` publish can't reach it. Two supported paths:
- **`studio-expose` bridge** (`STUDIO_EXPOSE=1` or run `studio-expose &`): a - **`studio-expose` bridge** (`STUDIO_EXPOSE=1` or run `studio-expose &`): a
`socat` relay from the container's external interface to its loopback, so a `socat` relay from the container's external interface to its loopback, so a
published `127.0.0.1:PORT` + `ssh -L PORT:127.0.0.1:PORT host` reaches it. published `127.0.0.1:PORT` + `ssh -L PORT:127.0.0.1:PORT host` reaches it.
- **`/studio --listen-all`** (pi-studio ≥ 0.9.60): binds `0.0.0.0`, so the
published port reaches it with no bridge; the printed URL still says
`127.0.0.1`. **Never on the same port as the bridge**: whichever starts
second fails with `EADDRINUSE`. `ss -ltnp | grep ":$STUDIO_PORT"` showing a
`socat` line means the bridge is up, so start Studio without `--listen-all`.
The real auth token comes from the `/studio` slash command (`/studio --status` The real auth token comes from the `/studio` slash command (`/studio --status`
to reprint), **not** from `studio-expose`. For Graphviz, use `dot-watch` → to reprint), **not** from `studio-expose`. Hand over the **whole** printed
PNG (Studio renders Mermaid natively and previews PNG, but not SVG/DOT). URL: since pi-studio 0.10 it also carries `studioTabState` + `hostedWorkspace`
(or `docPath`/`watchFile`/… for `--watch`), and a token-only URL is answered
`409`. The URL wraps across TUI lines; if a side panel's text lands inside the
copy, Studio answers *"This link looks damaged…"*. Studio must run on the
**published** port (only the laptop side of `ssh -L` is free to choose), and
`--port` on a running server only warns, so `/studio --stop` first. On the
Docker host, `curl -s http://127.0.0.1:$STUDIO_PORT/` answering `403` means
the chain is up; an empty reply means Studio is down or on another port.
For Graphviz, use `dot-watch` → PNG (Studio renders Mermaid natively and
previews PNG, but not SVG/DOT).
## 8. MemPalace is the shared brain ## 8. MemPalace is the shared brain