shenmux

module
v0.0.0-...-cce531b Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 14, 2026 License: BSD-3-Clause

README

shenmux

A terminal multiplexer. shenmux run owns a PTY on some machine; clients attach to it by name — a native TTY client, a local browser terminal, or a browser on another machine relayed through a controller. It is a development tool. Nothing here is deployed to the public Internet.

See it work

nix develop
make demo

It builds what it needs, starts a session, a controller, an agent and the local browser gateway, enrolls itself, and prints the URL to open. Open the URL it prints, not a bare 127.0.0.1:8787 — it carries an access token, and the port moves when something else already has 8787. That is a real shell in a real terminal renderer. Type in it.

make demo DEMO_ARGS=--detach leaves it running; scripts/demo.sh --stop stops it. The demo keeps its state in its own directory, so it will not disturb an enrollment you already have.

The pieces, by hand

shenmux run is the only thing that creates a PTY. Nothing else in this repository does — not the gateway, not the controller, not the agent. Start it first, on the machine that should own the shell:

./bin/shenmux run --session work

Attach from a terminal:

./bin/muxctl -session work            # -observe for read-only

Or attach from a browser on the same machine:

./bin/shenmux web --session work      # --listen 127.0.0.1:8787 by default

It prints a URL containing an access token, e.g. shenmux web url=http://127.0.0.1:8787/?token=.... Open that, not the bare address: a loopback port is not an authorization boundary, so without a token any local process can attach and take the input lease. --no-token opts out if you want that.

shenmux run starts a login shell and restarts it when it exits. Pass a command after -- for one shot, or --keepalive=false. shenmux status prints the config, the state directory, and the sessions this binary has started.

Control is exclusive and cannot be stolen

One client holds the input lease at a time. If another client has it, your "take control" request fails and you get a session you can watch but not type into. That is deliberate, not a bug. Detach the other client, or release control from it, and try again.

A browser tab left open from an earlier shenmux web process cannot reattach and grab the lease out from under you: every gateway process stamps an instance id into the page it serves and refuses a handshake carrying a different one. The stale tab gets a plain "reload the page" error instead of silently attaching.

Remote through a controller

Private network only. This uses plain HTTP and turns on a deliberately insecure browser-identity shortcut.

# controller host
./bin/shenmux controller --listen 127.0.0.1:8788 \
  --origin http://127.0.0.1:8788 --dev-browser-subject local-test

It prints one single-use enrollment code to stderr, good for ten minutes. On the host that owns the PTY:

./bin/shenmux run --session work
./bin/shenmux login --controller http://127.0.0.1:8788 --code CODE
./bin/shenmux agent --controller http://127.0.0.1:8788 \
  --transport relay --session work

The controller serves no browser UI. GET /sessions lists what the connected agents advertise — session names and whatever opaque --label key=value pairs the agent was started with — and /capabilities and /browser are there for a client to use. Teaching the real client (shenmux web) to attach through a controller is not done yet.

shenmux login refuses to enroll over an existing device identity: it names the device id at stake and stops rather than overwriting ~/.local/state/shenmux/state.json. Point it at a separate state directory to enroll alongside what you already have:

./bin/shenmux login --controller http://127.0.0.1:8788 --code CODE \
  --state-dir /tmp/other-enrollment

--force replaces the existing enrollment instead — this destroys its keypair, and the old device stays registered on the controller with nothing left here that can act as it. SHENMUX_STATE_DIR and SHENMUX_CONFIG_FILE do the same job as --state-dir/--config, but for every client command in a session at once, which is why make demo sets them rather than passing flags to each one.

Across hosts, replace loopback with a reachable private address and keep --origin equal to the URL the agent uses. shenmux login allows plain HTTP only for localhost or a bare IP; a named host must be HTTPS. --transport tailscale or --transport auto --direct tailscale://HOST:8788/ws sends the agent over a tailnet instead of the relay; auto falls back to the relay when the direct endpoint fails to connect, which is a reconnect, not stream migration.

What is actually true

Works: local sessions, the native client, the local browser terminal, enrollment, the relay path, tailnet transport, reconnect with backoff.

Does not:

  • The controller can read your terminal. Trusted relay mode is the default and the only mode any bundled client speaks. TLS would protect the wire, not the controller.
  • Blind mode has no client. The handshake, encryption and tests exist as library code. Nothing shipped here initiates it. login --trust-mode blind makes the agent reject every trusted stream, which is the point, but it leaves you with no browser that can reach it through a controller.
  • No production hosting and no multi-user story. The browser identity fallback trusts an X-Shenmux-Subject header. There is no TLS, no origin policy, no rate limiting, no admin workflow for grants or revocation. The checked-in Dockerfile builds shenmux, but its entrypoint still starts the legacy muxd shape and no deployment of it is tested. See docs/DEPLOYMENT.md.
  • The controller's session list is memory-only and believes the agent. It lists the names passed to shenmux agent --session/--sessions. Nothing reconciles that against processes that are actually running, and it is gone when the controller restarts.
  • No Windows PTY. Linux and macOS, Go 1.26+.
  • Nothing survives the session. Kill shenmux run or reboot its host and the PTY is gone. Checkpoints and deltas are in memory and are never written to disk. Earlier builds did write them: every accepted event was archived under <state-dir>/history, which defaulted to ~/.local/state/shenmux/history. That is gone. If a previous build left archives there, shenmux run names the directory once on startup and otherwise ignores it; nothing can restore one into a live session, so delete it when you see it.

The default build is pure Go: no CGO, no libzmq, no C toolchain. -tags libghostty swaps in the libghostty-vt terminal adapter and needs both.

Tests

nix develop --command make check   # web bundle drift guard, JS tests, Shen guards + gate, go test, vet, both builds
nix develop --command make race

internal/webui/app.bundle.js is committed and embedded in the binary, so an edit to the client source that skips npm run build ships nothing. make check catches that drift instead of quietly fixing it for you.

make test-deploy is a compile check, not a deployment test.

More

The Shen spec isn't a build-time check that disappears once code compiles: internal/shenguard/guards_gen.go calls the generated model at runtime (semanticModel.Call) for every session transition — attach, control, input, delivery — so it decides these outcomes as the session runs, not just checks them ahead of time.

Directories

Path Synopsis
cmd
muxctl command
muxd command
shenmux command
shenmux is the whole command surface: one binary, one PTY, and the verbs that reach it.
shenmux is the whole command surface: one binary, one PTY, and the verbs that reach it.
shenmux-gen command
shenmux-gen lowers the restricted Shen model into opaque Go guards.
shenmux-gen lowers the restricted Shen model into opaque Go guards.
shenmux-web command
internal
agent
Package agent bridges authenticated relay streams to sessions hosted by a local shenmux run process.
Package agent bridges authenticated relay streams to sessions hosted by a local shenmux run process.
appstate
Package appstate owns the small, versioned files used by the shenmux command.
Package appstate owns the small, versioned files used by the shenmux command.
policy
Package policy contains controller-side authorization and operational state.
Package policy contains controller-side authorization and operational state.
relay
Package relay contains the transport-neutral pieces of the outbound relay protocol.
Package relay contains the transport-neutral pieces of the outbound relay protocol.
shenmodel
Package shenmodel parses and evaluates the deliberately restricted, pure Shen subset used by shenmux.
Package shenmodel parses and evaluates the deliberately restricted, pure Shen subset used by shenmux.
transport
Package transport discovers optional direct paths used by the authenticated relay tunnel.
Package transport discovers optional direct paths used by the authenticated relay tunnel.
zmqx
Package zmqx exposes the small ZeroMQ surface used by shenmux.
Package zmqx exposes the small ZeroMQ surface used by shenmux.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL