docs(readme): pi-studio has --listen-all; the remote recipe describes the 0.10 URL
Lint / skill-floor (push) Successful in 9s
Lint / hadolint (push) Successful in 12s
Lint / doc-drift (push) Failing after 13s
Lint / actionlint (push) Successful in 22s

README "Using pi-studio" said pi-studio hard-binds 127.0.0.1 with no bind flag.
It has had /studio --listen-all (binds 0.0.0.0) since 0.9.60; listed now as path
C, with a warning not to combine it with the studio-expose bridge on one port:
with STUDIO_EXPOSE=1, `/studio --listen-all --port 8765` fails with
"listen EADDRINUSE: address already in use 0.0.0.0:8765" (measured, tor-ms22,
v1.11.2).

The remote recipe described a 0.9 URL. Since 0.10 the printed URL also carries
studioTabState + hostedWorkspace and a token-only URL is answered 409; the
recipe now says to open the whole URL, names the wrapped-URL copy hazard
("This link looks damaged..."), says Studio must run on the published port
(only the laptop's side of ssh -L is free) and that --port does not move a
running server, adds --watch <file>, and gives an on-host curl that tells
"chain up" (403) from "Studio down or on another port" (empty reply).

All of it measured on tor-ms22 2026-10-14 (pi-studio 0.10.1): tunnel through
the host sshd + headless browser load, both modes; --listen-all reached on the
container's eth0 (200 / 403 without token). Docs only; no pin changes.
This commit is contained in:
2026-10-14 18:45:46 +02:00
parent bf265f6d54
commit 1e4c238a83
2 changed files with 70 additions and 13 deletions
+26
View File
@@ -11,6 +11,32 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
---
## Unreleased
Docs only: this entry changes no pin.
### Fixed
- **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
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.
With `STUDIO_EXPOSE=1` the bridge holds the egress IP on `$STUDIO_PORT`
from boot, and `/studio --listen-all --port 8765` then fails with
`listen EADDRINUSE: address already in use 0.0.0.0:8765` (measured on
tor-ms22, v1.11.2).
- **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
a token-only URL is answered `409`. The recipe now says to open the whole
URL, and names the copy hazard: the URL wraps, a side panel's text can land
inside it, and Studio answers *"This link looks damaged…"*. It also says
Studio must run on the **published** port (only the laptop's side of
`ssh -L` is free to choose) and that `--port` does not move a running server
(`/studio --stop` first). Two more additions: `--watch <file>` for a rendered,
read-only preview, and an on-host `curl` that tells "chain up" (`403`) from
"Studio down or on another port" (empty reply). A remote session hit exactly
that: Studio was started on `18765` while only `8765` is published.
## v1.11.2 — 2026-10-14
**v1.11.1 was tagged and never published.** Run 724 failed one smoke assertion
+44 -13
View File
@@ -198,12 +198,11 @@ Inside a pi session in the container:
### Reaching the UI from your browser (the container caveat)
pi-studio **hard-binds its server to `127.0.0.1` inside the container**
(`index.ts`: `.listen(port, "127.0.0.1")`) and serves a tokenized URL.
There is no `--host`/bind flag. This matters for a container: a plain
By default pi-studio **binds its server to `127.0.0.1` inside the
container** and serves a tokenized URL. This matters for a container: a plain
`docker run -p 8765:8765` publish forwards to the container's *external*
interface, **not** its loopback, so it will not reach Studio. Two paths
work:
interface, **not** its loopback, so it will not reach a default Studio.
Three paths work (A and B keep Studio itself on loopback; C does not):
**A. Host networking (simplest — OrbStack / single-host, no bridge).**
Run the container with host networking so the container's loopback is the
@@ -215,8 +214,9 @@ services:
network_mode: host # container 127.0.0.1 == host 127.0.0.1
```
Then `http://127.0.0.1:8765/?token=…` works in a browser on the Docker
host. This is the most secure option (Studio never leaves loopback). Note:
Then the URL Studio prints (`http://127.0.0.1:8765/?token=…`) works in a
browser on the Docker host. This is the most secure option (Studio never
leaves loopback). Note:
host networking changes `host.docker.internal` semantics, so weigh it
against the LAN-jump SSH feature if you use that.
@@ -253,6 +253,19 @@ studio-expose & # bridges $STUDIO_PORT (default 8765); --help for detai
> its tokenized URL is the only auth. Keep the host-side publish on
> `127.0.0.1:` and use `ssh -L` for remote access. Default is **off**.
**C. `/studio --listen-all` (pi-studio ≥ 0.9.60, no bridge).** pi-studio has
its own opt-in flag: `/studio --no-browser --listen-all --port 8765` binds
`0.0.0.0`, so the published port reaches it without `socat`. The URL it
prints still says `127.0.0.1`, so it works verbatim through a same-port
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
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
> the container's egress IP on `$STUDIO_PORT` from boot, and `--listen-all` on
> that same port fails with `EADDRINUSE` (`0.0.0.0` includes that address).
> If the bridge is running, start Studio *without* `--listen-all`.
### Remote host (SSH / mosh)
When the Docker host is remote, keep Studio on localhost and forward the
@@ -298,20 +311,38 @@ Assuming the compose file publishes `127.0.0.1:8765:8765` (see method B):
TUI, not a shell command:
```
/studio --no-browser --port 8765
/studio --no-browser --port 8765 --watch notes.md # or: a rendered, read-only preview of a file
/studio --status # reprint the URL anytime
```
Copy the `http://…:8765/?token=<token>` it prints. **This** is where
the real token comes from — not `studio-expose`.
**This** is where the real token comes from — not `studio-expose`.
The port must be the **published** one (`8765` here): the tunnel, the
publish and the bridge all lead to it, and Studio on any other port is
unreachable from outside. `--port` does not move a running server (Studio
only warns), so run `/studio --stop` first to change it.
3. **On your laptop** — open the ssh port-forward alongside mosh:
```bash
ssh -L 8765:127.0.0.1:8765 user@docker-host
ssh -N -o ExitOnForwardFailure=yes -L 8765:127.0.0.1:8765 user@docker-host
```
4. **In your laptop browser** — open `http://127.0.0.1:8765/?token=<token>`
(keep the port and token verbatim; only the host part is `127.0.0.1`).
Only the **left** port is yours to choose. If `8765` is already taken on
the laptop (for example by the laptop's own devbox publish), use
`-L 18765:127.0.0.1:8765` and change only the port in the URL.
4. **In your laptop browser** — open the **whole** URL Studio printed. It
already says `127.0.0.1`, and it carries more than the token
(`token`, `studioTabState`, `hostedWorkspace` since pi-studio 0.10, or
`docPath`/`watchFile`/… for `--watch`). A URL cut down to just the token
is answered `409`.
The URL is long and wraps across TUI lines; a side panel next to it (such
as the pi-atelier sidebar) can end up inside the copied text, and Studio
then answers *"This link looks damaged…"*. Rejoin the lines without the
panel text, or widen the terminal and copy it again.
> **Order check:** nothing listens on the container's `127.0.0.1:8765`
> until step 2 runs. If the browser can't connect, verify Studio is up
> (`/studio --status`) and the bridge is running (`ps aux | grep socat`).
> **on the published port** (`/studio --status`) and the bridge is running
> (`ps aux | grep socat`). On the Docker host,
> `curl -s http://127.0.0.1:8765/` answers `403` when the whole chain is up;
> an empty reply means the bridge has nothing behind it, i.e. Studio is down
> or on another port.
> PDF export (`/studio-pdf`, `studio_export_pdf`) uses **`typst`**, shipped in
> the base image as the pandoc PDF engine (`pandoc --pdf-engine=typst`). For