docs(maestro): unified-config rollover in installer + INSTALL + spec (NAP-233)
ci / rust (push) Successful in 4m10s
ci / rust (pull_request) Successful in 4m22s

- Installer: check for a `[daemon]` section in the unified file (rather
  than the existence of the now-defunct `~/.config/maestrod/config.toml`)
  to decide whether to skip the configure prompt on re-runs. The
  configure prompt + maestrod_config_path() helper now reference
  `~/.config/maestro/config.toml`.
- docs/INSTALL.md: mention the unified file and the single-TOML shape.
- spec: update §Registry / §Daemon / §Auth to call out
  `[registry].bind_addr`, `[daemon].bind_addr`, `[shared].registry_url`,
  and `[shared].token` instead of the old per-app files.
This commit is contained in:
2026-05-26 09:36:08 +00:00
parent ed5743cd50
commit f061005c55
3 changed files with 33 additions and 17 deletions
+24 -10
View File
@@ -96,14 +96,15 @@ fn run(args: Args) -> Result<(), Box<dyn std::error::Error>> {
// 2. Offer to run `maestrod configure` now (must happen before systemd // 2. Offer to run `maestrod configure` now (must happen before systemd
// start, since the daemon refuses to run without registry_url / token). // start, since the daemon refuses to run without registry_url / token).
// Re-runs of the wizard auto-skip when the config already exists; that's // Re-runs of the wizard auto-skip when the unified config already has a
// what makes "re-run to update" pleasant. In non-interactive mode (--yes / // [daemon] section; that's what makes "re-run to update" pleasant. In
// no TTY) we also skip — `maestrod configure` would prompt for required // non-interactive mode (--yes / no TTY) we also skip — `maestrod
// fields and we don't want to surprise scripts. // configure` would prompt for required fields and we don't want to
let configured = if let Some(path) = maestrod_config_path() { // surprise scripts.
if path.exists() { let configured = if let Some(path) = maestro_config_path() {
if daemon_section_present(&path) {
eprintln!( eprintln!(
"\nconfig already exists at {} — skipping configure step.", "\n[daemon] section already present in {} — skipping configure step.",
path.display() path.display()
); );
eprintln!("(run `maestrod configure --force` to rewrite it.)"); eprintln!("(run `maestrod configure --force` to rewrite it.)");
@@ -175,15 +176,28 @@ fn install_crate(
Ok(()) Ok(())
} }
fn maestrod_config_path() -> Option<PathBuf> { fn maestro_config_path() -> Option<PathBuf> {
Some(dirs::config_dir()?.join("maestrod").join("config.toml")) Some(dirs::config_dir()?.join("maestro").join("config.toml"))
}
/// True iff the unified file exists and already has a `[daemon]` section.
/// Used to skip the configure prompt on re-runs without forcing the user
/// to manually delete a co-located `[registry]` / `[cli]` section first.
fn daemon_section_present(path: &Path) -> bool {
if !path.exists() {
return false;
}
let Ok(body) = std::fs::read_to_string(path) else {
return false;
};
body.lines().any(|l| l.trim_start().starts_with("[daemon]"))
} }
fn run_configure_step(args: &Args, interactive: bool) -> Result<bool, Box<dyn std::error::Error>> { fn run_configure_step(args: &Args, interactive: bool) -> Result<bool, Box<dyn std::error::Error>> {
let want = interactive let want = interactive
&& !args.no_configure && !args.no_configure
&& confirm( && confirm(
"Run `maestrod configure` now (writes ~/.config/maestrod/config.toml)?", "Run `maestrod configure` now (writes ~/.config/maestro/config.toml)?",
true, true,
)?; )?;
if want { if want {
+6 -4
View File
@@ -17,9 +17,10 @@ wizard, and cleans up — only `maestrod` and `maestro` end up persisted in
`~/.cargo/bin`. `~/.cargo/bin`.
**Updating is the same command.** Re-running `./install.sh` detects an **Updating is the same command.** Re-running `./install.sh` detects an
existing `~/.config/maestrod/config.toml` and existing systemd unit, skips the existing `[daemon]` section in `~/.config/maestro/config.toml` and an
configure prompt, refreshes the unit, and runs `systemctl --user restart existing systemd unit, skips the configure prompt, refreshes the unit, and
maestrod` instead of `start`. No `--update` flag needed. runs `systemctl --user restart maestrod` instead of `start`. No `--update`
flag needed.
Inside the script, `cargo install --git https://git.naps.pt/naps62/maestro …` Inside the script, `cargo install --git https://git.naps.pt/naps62/maestro …`
uses your already-configured git credentials for this host (HTTPS helper or uses your already-configured git credentials for this host (HTTPS helper or
@@ -39,7 +40,8 @@ The wizard:
1. Runs `cargo install` for `maestro-daemon` and `maestro-cli` (pinned to the 1. Runs `cargo install` for `maestro-daemon` and `maestro-cli` (pinned to the
committed `Cargo.lock` via `--locked`). committed `Cargo.lock` via `--locked`).
2. Optionally runs `maestrod configure` to drop a `~/.config/maestrod/config.toml`. 2. Optionally runs `maestrod configure` to populate `~/.config/maestro/config.toml`
(one shared TOML for `maestrod`, `maestro-registry`, and the `maestro` CLI).
3. On systemd hosts, optionally writes a user unit at 3. On systemd hosts, optionally writes a user unit at
`~/.config/systemd/user/maestrod.service`, reloads systemd, and offers to `~/.config/systemd/user/maestrod.service`, reloads systemd, and offers to
enable + start it. enable + start it.
@@ -83,7 +83,7 @@ Non-responsibilities:
- No persistent storage in the prototype. State is in-memory; daemons re-register on reconnect. - No persistent storage in the prototype. State is in-memory; daemons re-register on reconnect.
- No multi-user model. One shared secret, one effective user. - No multi-user model. One shared secret, one effective user.
Runs as a single Rust binary on a designated VM. Listens on a configured `bind_addr` (default `127.0.0.1:8080`; operator sets the actual reachable address — e.g. a Wireguard interface IP — in `~/.config/maestro/registry.toml`). Runs as a single Rust binary on a designated VM. Listens on a configured `bind_addr` (default `127.0.0.1:8080`; operator sets the actual reachable address — e.g. a Wireguard interface IP — in `[registry].bind_addr` of the unified config at `~/.config/maestro/config.toml`).
### Daemon (`maestrod`) ### Daemon (`maestrod`)
@@ -101,7 +101,7 @@ Non-responsibilities:
- Doesn't relay to other daemons. It only knows about local tmux. - Doesn't relay to other daemons. It only knows about local tmux.
- Doesn't manage its own auth tokens; the registry issues them. - Doesn't manage its own auth tokens; the registry issues them.
Runs as a single Rust binary on every host that holds sessions. Small footprint matters because some hosts are LXC containers. Listens on a configured `bind_addr` (default `127.0.0.1:8081`; operator sets the actual reachable address in `~/.config/maestrod/config.toml`). Also requires `registry_url` so it knows where to register on startup. Runs as a single Rust binary on every host that holds sessions. Small footprint matters because some hosts are LXC containers. Listens on a configured `bind_addr` (default `127.0.0.1:8081`; operator sets the actual reachable address in `[daemon].bind_addr` of the unified config at `~/.config/maestro/config.toml`). Also requires `[shared].registry_url` so it knows where to register on startup.
### Web client ### Web client
@@ -244,7 +244,7 @@ struct Session {
## Auth ## Auth
- Registry generates `MAESTRO_TOKEN` at first run, printed once, stored in `~/.config/maestro/registry.toml`. - Registry generates the shared token at first run, printed once, stored as `[shared].token` in `~/.config/maestro/config.toml` (env var name: `MAESTRO_SHARED_TOKEN`).
- Same token deployed to each daemon (config file or env var) and used in `Hello`. - Same token deployed to each daemon (config file or env var) and used in `Hello`.
- Web client gets a cookie from the registry on first visit. All registry API calls use it. - Web client gets a cookie from the registry on first visit. All registry API calls use it.
- Per-attach JWT issued by registry, signed with the shared secret, validated locally by the daemon. ~5 min expiry; client refreshes by re-requesting `GET /api/sessions/:id/connect-info`. - Per-attach JWT issued by registry, signed with the shared secret, validated locally by the daemon. ~5 min expiry; client refreshes by re-requesting `GET /api/sessions/:id/connect-info`.