Commit Graph
15 Commits
Author SHA1 Message Date
naps62-yoloandClaude Opus 4.8 c2f32779a3 style(maestro): cargo fmt — clear pre-existing drift blocking CI fmt check (NAP-328)
ci / rust (push) Failing after 1m58s
`cargo fmt --all -- --check` was already failing on `main` (CI run 149) due to
unformatted code in broker.rs and http.rs; e2e.rs carried the same drift. This
applies rustfmt to those three so the branch passes the fmt gate. No behavior
change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:31:42 +00:00
naps62-yoloandClaude Opus 4.8 6d7cd0b4c8 feat(maestro): spawn form defaults command to claude + prefills cwd to daemon home dir (NAP-328)
Two web spawn QoL improvements plus the wire support they need:

- Command field now prefills `claude` instead of an empty box.
- Working directory prefills the selected daemon's home dir, updating on
  daemon change unless the user has typed their own value.

The daemon resolves its `$HOME` via `dirs::home_dir()` at startup and
advertises it end-to-end: a new `home_dir` field on `DaemonOutbound::Hello`
flows through the registry's `register` into `DaemonInfo` (served on
`/api/daemons` and `/api/events`). The field is `#[serde(default)]`, so older
daemons that omit it deserialize to an empty string — backward compatible, and
the UI simply skips prefill when it's empty.

Hierarchical `/`-separated labels in the web sidebar already worked
(web/src/catalog.ts); no change there.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:07:53 +00:00
naps62-yoloandClaude Opus 4.8 1cdb126977 feat(maestro): drop CLI registry fallback for cross-daemon create/kill + e2e (NAP-327)
The home daemon now brokers cross-daemon create/kill itself, so the CLI only
falls back to the legacy registry HTTP path when the home daemon is
*unreachable* — not on 501/404. Removes the now-dead `is_remote_op_unsupported`
/ `is_not_found` / `has_status` predicates.

Adds an e2e (real tmux, two daemons + registry): POST to alpha with
daemon_id=beta brokers the create to beta and returns beta's session; a kill
via alpha reaches beta. Both verified against the registry catalog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:27:31 +00:00
naps62-yoloandClaude Opus 4.8 f3f696790c test(maestro): cover perimeter-mismatch boundary + broker error paths (NAP-270)
ci / rust (push) Failing after 5m25s
The asymmetric-VLAN security boundary and the broker's RPC error branches had
no tests, so a regression that leaked a JWT across perimeters or dialed across
them would ship undetected. Adds the required coverage:

registry (state.rs unit):
- perimeter_for_requires_both_id_and_addr
- resolve_attach_refuses_self_target
- resolve_attach_returns_unknown_session_for_orphan_id

daemon broker (broker.rs unit):
- broker_resolve_attach_propagates_registry_err
- broker_resolve_attach_times_out_when_no_reply (paused-clock, instant)

daemon e2e:
- broker_attach_returns_502_on_perimeter_mismatch (two daemons, different
  perimeters → structured 502, never dials across)

Enables tokio `test-util` in maestro-daemon dev-deps for the paused-clock test.
Unblocks NAP-272.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 00:04:16 +00:00
naps62-yoloandClaude Opus 4.8 ae588b76d5 fix(maestro): broker dials peer before WS upgrade so failures surface (NAP-265)
ci / rust (push) Failing after 5m59s
`broker_attach_ws` upgraded the client WS first and only then dialed the peer
inside `pump_broker_ws`; a dial failure (peer down, perimeter rejection, peer
verifier refusing the pair JWT — all reported pre-upgrade) just logged and
closed the socket, indistinguishable from a clean session end.

Move the `connect_async` ahead of `on_upgrade`: on failure return a structured
`502 peer_unreachable` (matching the other pre-upgrade error paths); on success
hand the connected upstream to `pump_broker_ws`.

Updated the allow_inbound-reject e2e to assert the handshake now fails with 502
instead of an opaque post-upgrade close.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 23:35:03 +00:00
naps62-yoloandClaude Opus 4.8 643da30f63 test(maestro): e2e for CLI broker path + registry compat fallback (NAP-244)
ci / rust (push) Successful in 10m42s
Drives the real `maestro` binary: one test points `MAESTRO_HOME_DAEMON_URL` at
a live daemon with no registry configured (proves the catalog read comes from
the home daemon), the other points it at a dead port and asserts the CLI warns
and falls back to the registry while still listing the session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 19:25:28 +00:00
naps62-yolo c599fcfdec test(maestro-tui): integration tests against real daemon subprocess (NAP-282)
ci / rust (push) Failing after 5m1s
`crates/maestro-e2e/tests/tui_integration.rs` adds three integration tests
that spin up a real registry + daemon via the existing maestro-e2e harness
and drive `CatalogStore` against the daemon's `/api/*` surface:

- `catalog_bootstrap_against_real_daemon` — WS connect + initial Snapshot
  carries the self-daemon.
- `post_session_propagates_through_events_ws` — POST /api/sessions →
  catalog WS surfaces SessionAdded within 2 s.
- `delete_session_propagates_removal` — DELETE /api/sessions/:id →
  catalog WS surfaces SessionRemoved within 2 s.

These exercise the real wire protocol (subprocess + tmux + axum + tungstenite)
without going through the full ratatui frontend, complementing the unit-test
TestBackend assertions on widgets.

The full App-driven e2e (input injection feeding the dispatch loop, asserting
vt100 contents) would need `App::run` to be backend-generic; deferring as a
follow-up since the integration layer here already proves the wire protocol
round-trip the NAP-282 acceptance asked for.
2026-05-28 22:37:22 +00:00
naps62-yolo ade0d49096 chore(maestro): retarget installer, e2e harness, and docs at unified maestro binary (NAP-245)
- Installer: one `cargo install maestro` instead of two; ExecStart becomes
  `~/.cargo/bin/maestro daemon`; user unit renamed `maestrod.service` →
  `maestro.service`; configure step is `maestro daemon configure`.
- E2E harness: both `Daemon` and `Registry` subprocess wrappers shell out to
  the unified `maestro` binary with the appropriate subcommand.
- State dir: `~/.local/state/maestrod` → `~/.local/state/maestro`.
- Docs: CLAUDE.md repo layout, design spec repo layout + "Daemon (`maestro daemon`)"
  heading, INSTALL.md (full rewrite of CLI references + new thin-builds section).
2026-05-27 10:43:51 +00:00
naps62-yoloandClaude Opus 4.7 a671e15114 feat(maestro): daemon WS /api/events for live catalog updates (NAP-248)
ci / rust (push) Failing after 7m28s
New module `local_events::LocalEventBus` (broadcast<RegistryEvent>) with
two publishers:

* `spawn_session_publisher` — translates SessionManager's own session
  events into the matching `RegistryEvent` variants (with daemon_id
  filled in for the SessionRemoved case).
* `CatalogMirror::apply` — re-publishes peer events after applying them
  to the local mirror, so subscribers see the post-apply state.

New `WS /api/events` route on the daemon: subscribes to the bus, sends
an initial `Snapshot` built from manager.snapshot() ∪ catalog state,
then forwards live deltas. `Lagged` follows the registry's convention
(client resyncs via REST).

Revert the REST-seed hack in web/src/events.ts — the WS endpoint now
exists, so `CatalogStore::start()` can go back to just `connect()`.

E2E: subscribe to a daemon's events WS, expect Snapshot then a
SessionAdded after POSTing /api/sessions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:01:08 +00:00
naps62-yoloandClaude Opus 4.7 53cc008e3b test(maestro): e2e for broker T2 + allow_inbound enforcement (NAP-247)
ci / rust (push) Successful in 7m28s
Two scenarios:

* broker_t2_attach_through_home_daemon: alpha + beta share perimeter
  "wg-test"; beta lists alpha in allow_inbound. Test attaches to beta's
  session via alpha's /api/attach/:id and verifies the PTY echoes back.
* broker_rejects_when_target_doesnt_allow_inbound: same setup but beta's
  allow_inbound is empty; the broker WS handshake succeeds at alpha
  (which has no reason to refuse) but beta drops the pair JWT, so the
  proxied stream closes without any PTY bytes flowing.

Extends maestro-e2e::Daemon with DaemonOptions and a spawn_at variant
that fixes the bind address, so the daemon's advertised private_addr
matches the port the broker will dial.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:32:37 +00:00
naps62-yoloandClaude Opus 4.7 a701d537a3 test(maestro): e2e for daemon catalog mirror (NAP-246)
ci / rust (push) Successful in 8m4s
Spins up registry + alpha + beta, creates a session on beta, verifies
alpha's `/api/sessions` and `/api/daemons` reflect the peer state within
5 s of the create round-tripping.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 19:18:31 +00:00
naps62-yolo 094cd1ffce fix(maestro): drop SHARED_ prefix from [shared] env vars
ci / rust (pull_request) Successful in 4m13s
The shared section is the implicit global scope, so its fields use
the bare `MAESTRO_<FIELD>` env-var spelling. Only `[daemon]` and
`[registry]` sections get section-prefixed env vars.

Renames:
- MAESTRO_SHARED_TOKEN        → MAESTRO_TOKEN
- MAESTRO_SHARED_REGISTRY_URL → MAESTRO_REGISTRY_URL

`MAESTRO_DAEMON_*` and `MAESTRO_REGISTRY_BIND_ADDR` are unchanged.

This restores the canonical spec name (`MAESTRO_TOKEN`) that operators
already use in their deployment scripts — NAP-233 had renamed it to
`MAESTRO_SHARED_TOKEN`, which silently broke daemon auth against
existing registry deployments still setting `MAESTRO_TOKEN` in env.

Constants renamed alongside: `ENV_SHARED_TOKEN` → `ENV_TOKEN`,
`ENV_SHARED_REGISTRY_URL` → `ENV_REGISTRY_URL`. `EnvOverrides` fields
shed their `shared_` prefix to match.

Touches: daemon/cli/registry crates, e2e harness, design spec.
2026-05-26 13:24:29 +00:00
naps62-yolo ed5743cd50 feat(maestro): wire maestro-registry to unified config (NAP-233)
Registry now reads `[shared].token` and `[registry].bind_addr` from the
unified file via maestro-config. First-run token generation does a
read-modify-write so `[shared]` / `[cli]` / `[daemon]` sections that
already exist on a co-located host stay intact.

Env-var renames:
- MAESTRO_TOKEN          → MAESTRO_SHARED_TOKEN
- MAESTRO_REGISTRY_CONFIG → MAESTRO_CONFIG

`MAESTRO_REGISTRY_BIND_ADDR` is unchanged. The startup banner now points
the operator at `MAESTRO_SHARED_TOKEN` when announcing a freshly
generated token.

e2e harness updated to use the new env var; auth.rs doc-comment updated
to reflect the new env-var name.
2026-05-26 09:34:13 +00:00
naps62-yolo 90127e0ed3 feat(maestro): wire maestro-daemon to unified config (NAP-233)
Daemon now loads `[shared].registry_url`, `[shared].token`, and the
`[daemon]` section from the unified file via maestro-config. The
registry-WS endpoint is derived from `[shared].registry_url` (scheme
swap + `/daemon`), so operators configure a single base URL instead of
duplicating the daemon-specific endpoint.

Env-var renames (all per the new `MAESTRO_<SECTION>_<FIELD>` convention):
- MAESTROD_REGISTRY_URL → MAESTRO_SHARED_REGISTRY_URL (and the value is
  now an http(s) base, not the ws daemon endpoint)
- MAESTROD_DAEMON_URL   → MAESTRO_DAEMON_URL
- MAESTROD_DAEMON_ID    → MAESTRO_DAEMON_ID
- MAESTROD_BIND_ADDR    → MAESTRO_DAEMON_BIND_ADDR
- MAESTROD_CONFIG       → MAESTRO_CONFIG
- MAESTRO_TOKEN         → MAESTRO_SHARED_TOKEN

`maestrod configure` now writes the unified file with read-modify-write
so any existing `[cli]` / `[registry]` sections on the same host are
preserved. The interactive prompts reflect the new shape: registry URL
is asked for as a base URL, and the token prompt names the new env var.

Missing-file behaviour is hard cutover: if the config doesn't exist and
not every required field comes from env, the daemon errors with a
message pointing at `maestrod configure` and explicitly noting that the
legacy paths (~/.config/maestrod/config.toml) are no longer read.

e2e harness updated to match: `Daemon::spawn` takes the registry's HTTP
base, and the spawned subprocess sees the renamed env vars.
2026-05-26 09:31:15 +00:00
naps62-yolo 91f252b4e2 feat(maestro): add maestro-e2e cross-process test harness (NAP-217)
ci / rust (push) Successful in 2m8s
New `crates/maestro-e2e` exercises the full headless stack — registry,
two daemons, real HTTP+WS — as actual subprocesses.

Harness pieces:

- `Registry::spawn` and `Daemon::spawn` launch the real built binaries
  with `env_clear()` plus an isolated `$HOME`, `XDG_CONFIG_HOME`,
  `XDG_STATE_HOME`, and (for daemons) `TMUX_TMPDIR`. Drop guards kill
  the child and clean the temp dirs.
- `Daemon::stop` + `Daemon::restart` swap the process without losing
  the state dirs, so the recovery test exercises a real tmux
  re-adoption.
- A `build::workspace_target_bin` helper re-invokes cargo from the
  test process to build `maestrod` / `maestro-registry` on demand
  (cargo only ships libs as path-dep of e2e by default).
- `TestClient` wraps reqwest + tokio-tungstenite for the public
  HTTP+WS surface — list/spawn/kill/connect-info/attach + events WS.

Scenarios covered (`tests/e2e.rs`):

- list sessions from two daemons through one registry
- multi-attach: two WS clients see the same PTY output
- worktree with `cleanup_on_kill: true` is removed after kill
- daemon restart: catalog drops then repopulates from `recover()`
- concurrent spawns all get distinct ids

All five pass in ~0.7s in parallel; cleanup confirmed (no leftover
processes or temp dirs).
2026-05-26 07:38:19 +00:00