Cross-host (T2-brokered) attach fails when no shared perimeter is configured: the daemon rejects the upgrade with a structured 502 (no_perimeter_candidate / no_local_perimeter / perimeter_mismatch / peer_unreachable), but maestro-web's WS proxy had already upgraded the browser socket and merely dropped it on the upstream failure — the attach pane flashed and vanished with no reason shown. - maestro-web proxy: on a failed upstream handshake, relay the daemon's error message to the browser as an application close frame (4502 + reason) instead of a bare drop; parse the structured JSON error body. - web attach.ts: on a 4000+ close with a reason, show it in the pane header and terminal body and keep the pane up, rather than auto-closing. - daemon: WARN at startup when no perimeter_id/private_addr is configured, since cross-host attach is then unavailable. - docs/INSTALL.md: document the required matching perimeter_id + per-host private_addr for cross-host attach. Closes #39 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 KiB
Installing Maestro
The repo is private, so grab install.sh while signed in to gitea, then run it:
- Open https://git.naps.pt/naps62/maestro/src/branch/main/install.sh, click
Raw, save as
install.sh. Or from any existing clone:git show origin/main:install.sh > install.sh - Run it:
chmod +x install.sh && ./install.sh
The bootstrap script builds maestro-installer into a temp directory, runs the
wizard, and cleans up — only the unified maestro binary ends up persisted in
~/.cargo/bin.
Updating is the same command. Re-running ./install.sh detects an
existing [daemon] section in ~/.config/maestro/config.toml and an
existing systemd unit, skips the configure prompt, refreshes the unit, and
runs systemctl --user restart maestro instead of start. No --update
flag needed. The restart keeps your sessions: the unit sets
KillMode=process, so systemd signals only the daemon — the tmux server (and
every session) survives, and the new daemon re-adopts them on start. A unit
written before this default lacks the line; re-run ./install.sh to refresh it.
Inside the script, cargo install --git https://git.naps.pt/naps62/maestro …
uses your already-configured git credentials for this host (HTTPS helper or
SSH agent). If only SSH is set up:
MAESTRO_REPO_URL=ssh://git@git.naps.pt/naps62/maestro ./install.sh
Pass flags through to the wizard after ./install.sh:
./install.sh --yes --no-systemd
The wizard:
- Runs
cargo installfor the unifiedmaestrocrate (pinned to the committedCargo.lockvia--locked). The same binary hosts the CLI, the per-host daemon (maestro daemon), and the always-on registry (maestro registry), gated by Cargo features. - Optionally runs
maestro daemon configureto populate~/.config/maestro/config.toml(one shared TOML for the CLI, daemon, registry, and web). - On systemd hosts, optionally writes a user unit at
~/.config/systemd/user/maestro.service, reloads systemd, and offers to enable + start it.
The binary lands in ~/.cargo/bin/maestro — make sure that's on your $PATH.
Thin builds for single-role hosts
The default install pulls in everything (cli + daemon + registry + tui).
Single-role hosts can opt out of unused dependencies:
# Registry server — no tmux/PTY/jwt-verifier code:
cargo install --git https://git.naps.pt/naps62/maestro \
--no-default-features --features registry --locked maestro
# Daemon-only host — no embedded web assets, no registry HTTP surface:
cargo install --git https://git.naps.pt/naps62/maestro \
--no-default-features --features daemon --locked maestro
# CLI-only client (no local daemon, no TUI):
cargo install --git https://git.naps.pt/naps62/maestro \
--no-default-features --features cli --locked maestro
The wizard always installs the default-feature build.
The terminal UI: maestro tui
maestro tui is the keyboard-and-mouse driven terminal UI client. It ships
in the default feature set, so a vanilla cargo install maestro builds it.
Drop the tui feature to skip it on hosts that don't need the UI surface
(--no-default-features --features cli,daemon).
It talks only to the local maestro daemon over http://127.0.0.1:7070
by default; peer daemons reach you transparently via the catalog mirror.
Override the daemon URL with --daemon-url or $MAESTRO_DAEMON_URL.
The [tui] section in ~/.config/maestro/config.toml (or whatever path you
pass via the global --config flag) controls the keymap. Shipped defaults
cover every v1 action — only override what you want to change:
[tui]
leader = "ctrl-a" # one byte that escapes Insert mode
chord_timeout_ms = 1000 # how long to hold a partial chord like `g`
[tui.keymap.normal]
"i" = "EnterInsert"
"s" = "OpenSpawnModal"
"x" = "KillHighlighted"
"?" = "ShowHelp"
"ctrl-t" = "NewTab"
"g t" = "NextTab"
"ctrl-w h" = "FocusSidebar"
"enter" = "AttachHighlighted"
[tui.keymap.insert]
# (leader is added automatically as LeaveInsert; only override here when you
# want extra Insert-mode bindings)
The full action vocabulary lives in
docs/superpowers/specs/2026-05-28-maestro-tui-design.md §3.8.
Adding the browser UI (optional)
maestro-web is a separate binary that serves the SPA bundle and reverse-
proxies API calls to a co-located maestro daemon.
Interactive install (default path). The wizard now offers it as an optional step right after the daemon flow:
Optional: install the browser UI (`maestro-web`)?
Install maestro-web? [Y/n]
Press Y and it cargo installs maestro-web, drops a
~/.config/systemd/user/maestro-web.service unit (ordered after
maestro.service), reloads systemd, and offers to enable + start it. Re-runs
of ./install.sh detect the existing unit and refresh + restart silently.
Pass --no-web to skip the prompt entirely (headless hosts), or --yes
with --no-web for non-interactive daemon-only installs. (Bare --yes
without --no-web skips the web step too — opt in by re-running once
interactively.)
Frontend build prerequisite. maestro-web embeds the SPA bundle at
compile time, so before running the installer:
pnpm -C web install --frozen-lockfile
pnpm -C web build
Or just web from a workspace checkout.
Defaults. bind_addr = 127.0.0.1:8080, daemon_url = http://127.0.0.1:7070. The [web] section of
~/.config/maestro/config.toml overrides both — see
crates/maestro-web/README.md.
Trust model. Anyone who can reach the bind address can use it. Pick
127.0.0.1 for local-only, a WireGuard/LAN address for one-segment access,
never a public IP without adding auth first.
Headless hosts (servers, dev VMs) skip the prompt with --no-web. Hosts
where you want browser access answer yes — both binaries end up in
~/.cargo/bin.
Flags
| Flag | Purpose |
|---|---|
--git <url> |
Install from a different git repo (default: this one). |
--branch <name> |
Pin a branch with --git. |
--path <dir> |
Install from a local workspace checkout (--path + -p maestro). |
--no-locked |
Drop --locked from cargo install (resolves a fresh dep tree). |
--exec-start <path> |
Override the systemd unit's ExecStart (default ~/.cargo/bin/maestro daemon). |
--no-systemd |
Skip the systemd step even if detected. |
--no-configure |
Skip the maestro daemon configure step. |
--no-web |
Skip the optional maestro-web install step. |
--web-exec-start <path> |
Override the web unit's ExecStart (default ~/.cargo/bin/maestro-web). |
-y, --yes |
Non-interactive: install binaries + drop systemd unit + reload + enable, but don't auto-run configure or auto-start the unit (those need real config). Also skips the web step unless an existing maestro-web.service is being refreshed. |
Where tmux sockets land
maestro daemon defaults TMUX_TMPDIR to $XDG_RUNTIME_DIR (= /run/user/$UID)
so a plain tmux ls in your shell — which typically uses the same env var
— sees maestro-managed sessions. Override in
~/.config/maestro/config.toml:
[daemon]
tmux_tmpdir = "/tmp"
Pick /tmp if the daemon must outlive your login session and you can't run
loginctl enable-linger (the runtime dir is wiped on logout, taking the
tmux sockets with it). The env var MAESTRO_DAEMON_TMUX_TMPDIR takes
precedence over the file.
Cross-host attach (perimeter setup)
Attaching to a session that lives on another host is brokered over the "T2"
transport, which only works when both daemons advertise a matching
perimeter_id and the session's owner advertises a reachable
private_addr. These default to unset, and without them every cross-host
attach fails — the registry resolves the session but returns no perimeter
candidate, so the attaching daemon rejects it (no_perimeter_candidate) and the
browser pane shows the reason and closes. Same-host attach is unaffected.
Set both on every host in ~/.config/maestro/config.toml:
[daemon]
# Identical on every daemon that should reach each other — name it after your
# VPN/LAN segment. Two daemons broker to each other only when these match.
perimeter_id = "wg-home"
# THIS host's address as its peers reach it on that perimeter (host:port of the
# daemon's bind_addr). Per-host — never the same value on two daemons.
private_addr = "10.0.0.7:7070"
It's both-or-neither: setting only one is ignored with a startup warning. A
daemon started with no perimeter configured logs a WARN saying cross-host
attach is unavailable. (T1 direct-WSS was dropped — NAP-235 — and T3 relay isn't
implemented — NAP-238 — so T2 is currently the only cross-host path.)
Peer attach trust model
There is no per-peer allow-list. The operator's network perimeter (VPN/firewall)
is the trust boundary for which hosts can connect; every daemon reachable on
the perimeter may broker-attach to any other. Attach is still authenticated: the
registry mints a short-lived, pair-scoped JWT bound to the target daemon and the
specific session (iss/aud/sid/exp), which the target verifies — so a
reachable host still can't attach to a session it wasn't handed, or replay a
token elsewhere. (NAP-339 dropped the earlier [daemon.policy].allow_inbound
gate; re-introducing a per-pair allow-list later is a verify-time change.)
What the systemd unit looks like
The wizard writes a user unit (systemctl --user), since maestro daemon
runs per-user and uses tmux sockets under $XDG_RUNTIME_DIR. Body:
[Unit]
Description=Maestro per-host daemon
Documentation=https://git.naps.pt/naps62/maestro
After=network-online.target
[Service]
Type=simple
ExecStart=%h/.cargo/bin/maestro daemon
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
Check status / logs:
systemctl --user status maestro
journalctl --user -u maestro -f
Running the wizard without the bootstrap script
If you'd rather skip the script:
cargo install --git https://git.naps.pt/naps62/maestro maestro-installer
maestro-install
This leaves maestro-installer permanently in ~/.cargo/bin — the bootstrap
script avoids that by installing to a scratch dir.
Fully manual install (no wizard)
cargo install --git https://git.naps.pt/naps62/maestro --locked maestro
maestro daemon configure
maestro daemon # foreground; or wire your own service manager
Non-Linux hosts
The wizard works on macOS for the cargo install + maestro daemon configure
parts. The systemd step is automatically skipped (probe of
/run/systemd/system). launchd integration is not yet supported — start
maestro daemon from your shell or your own service manager.