Commit Graph
31 Commits
Author SHA1 Message Date
naps62-yoloandClaude Opus 4.8 cc8412ca59 feat(maestro): spawn protocol types + relay + daemon HTTP for recents/gitroot/presets (#26)
Expose the daemon-side spawn data (#24 recents, #25 gitroot, #22 presets) to
the web, mirroring the NAP-329 ListDir broker plumbing end to end so it works
across federated hosts.

Wire (maestro-protocol): `RecentDir`/`RecentDirs`/`GitRoot` payloads;
`DaemonInbound::RecentDirs`/`GitRoot` requests + `ForwardRecentDirs`/
`ForwardGitRoot` relay pairs + their `*Result` replies on both enums. All
forward-compatible via the existing `RpcResult` + `#[serde(other)] Unknown`.

Relay (broker + registry + registry_client): home daemon → registry → owning
daemon → back, with the same fail-fast-while-disconnected + RAII pending-entry
cleanup discipline as ListDir. The owning daemon answers RecentDirs from its
spawn history and GitRoot by resolving the path against its home dir
(shared `dirlist::resolve`) then walking up via `gitroot::find_root`.

Daemon HTTP (maestro-daemon):
- GET /api/daemons/:id/recent-dirs → RecentDirs
- GET /api/daemons/:id/gitroot?path= → GitRoot
- Presets ride GET /api/web/info (the documented DaemonInfo sibling) so the
  form needs no extra round-trip; resolved once at startup from [spawn].presets
  (a bad preset list now refuses startup).

Tests: protocol round-trip + forward-compat for the new variants; broker
forward round-trips + fail-fast; HTTP handler tests (self recents/gitroot,
presets advertisement, peer 503 without a registry link).

Closes #26

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:09:18 +00:00
naps62-yolo eb2162889e Merge branch 'updates'
ci / rust (push) Successful in 4m3s
2026-06-19 07:07:59 +00:00
naps62-yoloandClaude Opus 4.8 b6833ede43 refactor(maestro): drop allow_inbound policy gate, keep JWT session binding (NAP-339)
The peer (T2) attach path had a per-pair access-control layer
(`[daemon.policy].allow_inbound`): a target-side allow-list of daemon ids
permitted to broker-attach, enforced inside JWT verification, hot-reloadable via
SIGHUP / `POST /api/policy/reload`, with a `maestro policy` CLI. On a flat-trust
perimeter (the operator's VPN/firewall is the boundary for which hosts connect)
that granularity is unused, and deny-by-default made it config friction —
cross-daemon attach silently failed until every peer was listed.

Remove the allow-list; keep the registry-minted pair-attach JWT and ALL its
binding checks (iss=registry, aud=target, sid=session, exp). The firewall gates
host->port reachability; it does not stop a reachable host from attaching to a
session it wasn't handed or replaying a token elsewhere — that's the JWT's job
and it stays. Only the `sub in allow_inbound` check is dropped.

Removed: DaemonPolicySection / [daemon.policy].allow_inbound (config + daemon),
policy.rs (SIGHUP listener + GET/POST /api/policy), the `maestro policy` CLI,
PairAttachVerifier's allow-list field/reload/snapshot + JwtError::NotAllowed,
and the e2e broker_rejects_when_target_doesnt_allow_inbound test. The NAP-265
pre-upgrade-502 path stays covered by broker_attach_returns_502_on_perimeter_mismatch.

Also dropped the unit's ExecReload=kill -HUP: with no SIGHUP handler left,
default SIGHUP disposition is terminate, so wiring `systemctl reload` to it would
kill the daemon. Re-introducing a per-pair gate later is a verify-time change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 07:07:55 +00:00
naps62-yolo d1f32f6580 Merge branch 'updates'
ci / rust (push) Successful in 3m50s
2026-06-18 15:04:28 +00:00
naps62-yoloandClaude Opus 4.8 02475191df fix(maestro): registry writes each session change once; drop dual-write (NAP-268)
A registry-driven CreateSession/KillSession was reported to the registry twice:
the RPC reply (applied eagerly in create_session/kill_session) AND the async
SessionAdded/SessionRemoved the daemon emits on its own SessionManager stream.
Both landed, so the registry's catalog write had to be idempotent to absorb the
duplicate — masking bugs where the async event never arrives and setting a trap
for any future non-idempotent event (counters, partial updates).

Drop the duplicate at the source: the daemon parks the session id of an
RPC-initiated create/kill in a per-connection RpcCovered set and swallows the
matching async event (consumed exactly once) before it reaches the registry.
Spontaneous changes — sessions created via the daemon's local HTTP API, or
reaped on their own — are not parked and still forward, since the registry has
no other way to learn about them. A lag resync clears the set (the snapshot is
full truth). The registry's eager apply is now the authoritative single write,
not a race-bandaid; idempotency is no longer a correctness invariant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 15:04:24 +00:00
naps62-yoloandClaude Opus 4.8 75d162dea3 refactor(maestro): share cookie name + extraction across CLI and registry (NAP-273 F)
ci / rust (push) Successful in 6m45s
Promote `COOKIE_NAME` and a robust `extract_cookie` (iterates every `;`-pair,
so it works on both `Cookie:` and `Set-Cookie:` headers regardless of position)
into `maestro-protocol`. The registry re-exports them; the CLI drops its weaker
`extract_cookie_value` (which only inspected the first pair) and its duplicate
`COOKIE_NAME`. Both ends of the wire now agree on one definition.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:35:30 +00:00
naps62-yoloandClaude Opus 4.8 71a45db0a4 refactor(maestro): single-source broadcast capacities + extract cleanup_record (NAP-273 D/E)
D: the four independent `= 256` broadcast-channel constants (session events,
local event bus, PTY fan-out, registry events) now derive from one shared
`maestro_config::BroadcastTuning::DEFAULT`, so the coincidence is explicit and
there's a home for per-deployment tuning later.

E: `kill()` and `reap_dead_session()` hand-rolled the same FIFO/sidecar/
worktree/broadcast teardown, differing only in whether tmux still had a session
to kill. Extracted into `cleanup_record(inner, id, record, kill_tmux)`. kill's
sidecar-delete is now best-effort (the session is already out of the
authoritative map), matching the reaper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:35:30 +00:00
naps62-yoloandClaude Opus 4.8 881113d121 refactor(maestro): consolidate bind_addr + state_dir config helpers (NAP-273 B/C)
Add `parse_bind_addr`, `default_state_dir(subdir)`, and an `InvalidBindAddr`
variant to `maestro-config`, and route the daemon, registry, and web config
loaders through them. Each crate's hand-rolled `InvalidBindAddr` variant and
duplicated XDG state-dir resolution are gone; their loaders now `?` the shared
helper, which converts into their existing transparent `Inner` variant.

Also adds the shared `BroadcastTuning` struct (used by the next commit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:35:30 +00:00
naps62-yolo 498cb471b7 feat(maestro): daemon+registry ListDir RPC + broker relay + HTTP dirs endpoint (NAP-329)
Also fixes two pre-existing test call sites that NAP-328 left passing the
old arity to register()/state_for() after adding home_dir.
2026-06-15 21:18:09 +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 daeacc750f feat(maestro): registry relays Forward{Create,Kill} RPC for cross-daemon ops (NAP-327)
Adds the home-daemon → registry control-link RPC that brokers a cross-daemon
session create/kill without putting any session bytes on the registry: the
owning daemon still does the tmux spawn, the registry just relays request and
reply.

- protocol: `DaemonOutbound::{ForwardCreate,ForwardKill}` +
  `DaemonInbound::{ForwardCreateResult,ForwardKillResult}` (+ round-trip tests).
- registry: `Registry::{forward_create,forward_kill}` reuse the existing
  `create_session`/`kill_session` R→D machinery and route the reply back to the
  home daemon; transport faults map to stable `ErrorInfo` codes. Wired into the
  `WS /daemon` dispatch.

Next: daemon-side broker handle + local_api wiring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:21:11 +00:00
naps62-yoloandClaude Opus 4.8 eb6aeb4241 refactor(maestro): typed perimeter advertisement replaces Option pair (NAP-272)
ci / rust (push) Failing after 5m58s
`(perimeter_id, private_addr)` was carried as two `Option<String>`s with an
unwritten "both Some or both None" invariant, enforced ad-hoc at every reader
and silently violable (`with_perimeter(Some, None)`).

Collapse it to `Option<PerimeterCandidate>` (reusing the existing protocol
type, so it flows unconverted from Heartbeat → registry → AttachCandidates):

- protocol: `Heartbeat { perimeter: Option<PerimeterCandidate> }` (empty still
  serializes to the legacy `{"type":"Heartbeat"}` shape).
- registry: `DaemonEntry.perimeter`, `update_perimeter`, `perimeter_for` lose
  the defensive AND-check; `build_attach_candidates` uses it directly.
- daemon: `RegistryClient.with_perimeter(Option<PerimeterCandidate>)`; the
  both-or-neither rule now lives once, in `DaemonConfig::perimeter()`, which
  warns and drops a half-configured pair instead of advertising it.

Unblocked by NAP-270's coverage; broker T2 + perimeter-mismatch e2e green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 00:32: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 939d67615a fix(maestro): bind registry JWTs to issuer/audience to stop token confusion (NAP-266)
ci / rust (push) Failing after 6m0s
Cookie and pair-attach JWTs are signed with the same shared secret. Neither
verifier bound the token's purpose, so a token minted for one could in principle
validate as the other.

- Add a canonical `REGISTRY_ISSUER` to maestro-protocol; registry re-exports it.
- Auth cookies now carry `iss = maestro-registry` + a dedicated `aud =
  maestro-cookie`; `verify_cookie` binds both, so a pair-attach JWT (daemon-id
  audience) is rejected.
- `PairAttachVerifier::verify` now binds `iss = REGISTRY_ISSUER`, rejecting any
  token not minted by the registry.

Tests: pair-verifier rejects a foreign issuer; verify_cookie rejects a
pair-attach JWT. Existing cookie + broker e2e flows unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 22:44:25 +00:00
naps62-yoloandClaude Opus 4.8 287afa26ea fix(maestro): remove pending-RPC entries on caller cancellation (NAP-259)
ci / rust (push) Successful in 7m22s
`BrokerHandle::resolve_attach` (daemon) and `Registry::create_session` /
`kill_session` (registry) insert a pending oneshot into a map, then await
the reply. If the awaiting future is cancelled — e.g. an axum handler
dropped because the browser closed the tab or aborted the XHR — the entry
was never removed, leaking one map slot per cancelled request and growing
unboundedly under UI-driven retry traffic.

Introduce a small RAII `PendingGuard` that removes the entry on drop, so
cleanup happens on every exit path including cancellation. Because a sync
`Drop` can't lock a tokio mutex, move the pending maps to
`std::sync::Mutex` (only ever locked for a tiny insert/remove, never held
across an await): broker `pending`, and the registry's `pending_create` /
`pending_kill` lifted out of the async-locked `State`. Removal is
idempotent, so the happy path (reply already taken by `complete_*`) and
the guard's drop don't conflict; `await_rpc` no longer needs its manual
timeout cleanup.

Tested: cancelling an in-flight `resolve_attach` (broker) and
`kill_session` (registry) leaves the pending map empty.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 21:10:10 +00:00
naps62-yoloandClaude Opus 4.8 599f3ed178 fix(maestro): fence registry deregister with a per-connection token (NAP-258)
ci / rust (push) Successful in 7m41s
`Registry::deregister` removed the catalog entry unconditionally. If a daemon's
old WS handler hit heartbeat timeout *after* the daemon had reconnected on a
new socket, the stale handler's deregister wiped the freshly-installed live
entry and broadcast DaemonRemoved + SessionRemoved for all its sessions.

`register` now mints a per-connection fencing token (uuid) stored on the
DaemonEntry and returned to the handler, which holds it for the connection's
life. `deregister(daemon_id, token)` only tears down the entry when the stored
token matches, so a superseded handler's late deregister is a no-op.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 21:02:54 +00:00
naps62-yoloandClaude Opus 4.8 8b4cdfd650 fix(maestro): tolerate unknown WS frame types instead of dropping the connection (NAP-269)
ci / rust (push) Successful in 14m9s
`dispatch_text` (registry) and `handle_inbound` (daemon) treated any
frame that didn't deserialize into a known `DaemonOutbound`/`DaemonInbound`
variant as fatal: the registry returned `false` (breaking the handler
loop and deregistering the daemon, wiping its catalog), and the daemon
propagated the serde error out of the read loop. A newer peer emitting a
not-yet-merged variant would therefore get stuck in a tight
disconnect/reconnect loop.

- Add a `#[serde(other)] Unknown` catch-all to both enums so unknown
  `type` tags decode instead of erroring.
- Log-and-skip the `Unknown` variant on both sides.
- Also log-and-skip genuinely malformed frames rather than tearing down
  the connection, so one bad frame can't wipe a daemon's catalog.

Tested: unknown/malformed frames keep the connection alive (registry
handler), and unknown `type` decodes to `Unknown` (protocol round-trip).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 19:30:36 +00:00
naps62-yoloandClaude Opus 4.8 212ebbf120 fix(maestro): stop embedding web bundle in the registry
The registry serves no UI — that is maestro-web's job (connection-
orchestration spec §"UI hosting"). The rust-embed of web/dist was
legacy from NAP-218, before maestro-web was extracted (NAP-241), and it
made the registry (and thus the umbrella maestro binary) fail to compile
whenever web/dist was absent — e.g. a fresh cargo install --git after
the tracked .gitkeep was dropped. Remove web_assets.rs, the rust-embed
dep, and the / + /assets routes; the registry is now JSON/WS API only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 06:40:23 +00:00
naps62-yolo 9ce6303293 refactor(maestro): wire --config flag through to maestro registry
ci / rust (push) Failing after 2m13s
ci / rust (pull_request) Successful in 4m54s
`registry::run` now accepts an `Option<PathBuf>` config override so the
umbrella binary's `--config` flag (and its clap-bound `MAESTRO_CONFIG`
env var) reach the registry, mirroring how the daemon already consumed
the override.
2026-05-27 10:45:16 +00:00
naps62-yolo 35482118b1 feat(maestro): unify maestro/maestrod/maestro-registry into single binary (NAP-245)
New `crates/maestro/` umbrella binary hosts the CLI, daemon, and registry
behind Cargo features `cli`, `daemon`, `registry` (default = all three).
Library crates keep their structure; per-binary `main.rs` files are removed
and their dispatch logic is exposed as public functions:

- `maestro_cli::dispatch(command, config, registry_url)`
- `maestro_daemon::cli::dispatch(config, cmd)` (with public `DaemonCmd`/`ConfigureArgs`)
- `maestro_registry::run()`

Subcommand surface:

- `maestro list / spawn / attach / kill / set-registry / policy` (unchanged)
- `maestro daemon [run|configure]` (was `maestrod`)
- `maestro registry` (was `maestro-registry`)

Hard cutover — old binary names are not preserved.

Verified: default-feature build, plus `--no-default-features --features {cli|daemon|registry}`
each build clean with `-D warnings`.
2026-05-27 10:35:41 +00:00
naps62-yoloandClaude Opus 4.7 698032d870 feat(maestro): registry pair-JWT issuer + ResolveAttach handler (NAP-247)
* DaemonEntry stores the daemon's most recent perimeter_id / private_addr
  from heartbeats, with `update_perimeter` and `perimeter_for` accessors.
* issue_pair_attach_jwt mints the iss/sub/aud/sid/exp pair claim shape
  (60s TTL) — verified by the target daemon.
* Registry::resolve_attach assembles AttachCandidates: T1 stays None
  (NAP-235 canceled), T2 carries the target's perimeter when known, T3
  stays None (NAP-238 pending), plus the pair JWT.
* WS handler routes the new DaemonOutbound::ResolveAttach to the new
  method and persists perimeter info on every Heartbeat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:32:18 +00:00
naps62-yoloandClaude Opus 4.7 2e6b7ac356 feat(maestro): registry fan-out of catalog events to daemons (NAP-246)
Each daemon connection now also subscribes to the registry's event broadcast.
A per-connection task pumps peer-filtered events through the existing
outbound mpsc as `CatalogEvent` frames; the daemon side mirrors them
locally (NAP-246).

Two pre-existing race fixes that NAP-246's extra traffic exposed:

* `create_session` / `kill_session` now eagerly mirror Ok results into
  state.sessions (via the same `add_session` / `remove_session` calls the
  daemon's async `SessionAdded` / `SessionRemoved` will hit later — both
  are idempotent). HTTP callers no longer see a transient empty catalog
  immediately after a successful POST.
* Catalog frames flow through the same select! arm as RPC frames so the
  WS sink+stream coupling axum imposes doesn't stall the inbound reader.

Updates daemon_ws.rs tests to skip CatalogEvent frames when asserting on
RPC frames.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 19:18:18 +00:00
naps62-yoloandClaude Opus 4.7 9a8109559b feat(maestro): extend Heartbeat with perimeter_id + private_addr (NAP-242)
Convert DaemonOutbound::Heartbeat from unit to struct variant with two
optional fields advertising the daemon's T2 reachability. Old payloads
({"type":"Heartbeat"} with no fields) still parse — both fields default
to None — and a new daemon that doesn't populate them serializes back to
the same minimal shape.

Update the daemon's heartbeat ticker and the registry's match arm to the
new shape. Population from daemon config + registry persistence lands
with the broker logic in NAP-243.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 17:46:05 +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-yoloandClaude Opus 4.7 2a902b713e feat(maestro): build web bundle into Docker image
ci / rust (push) Successful in 1m13s
NAP-218. Add a node:20 stage to the registry Dockerfile that runs
`pnpm install && pnpm build` against `web/`, then copies the
resulting `dist/` into the Rust builder so rust-embed picks it up.
Single-binary deploy still — no separate static-asset volume.

`.dockerignore` keeps `web/node_modules` and `web/dist` out of the
build context (the node stage produces them fresh) while letting the
sources through.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 09:57:26 +00:00
naps62-yoloandClaude Opus 4.7 077e9e958e feat(maestro): embed and serve web bundle from registry
NAP-218. The registry's `GET /` now serves the built web bundle from
`web/dist/` (embedded into the binary at compile time via rust-embed)
instead of the NAP-215 placeholder, and `GET /assets/*` serves the
hashed JS/CSS chunks Vite produces.

When `web/dist` is empty (fresh checkout without `pnpm build`), a
small fallback page tells the operator to run the JS build. That
keeps `cargo run -p maestro-registry` usable without a node
toolchain installed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 09:57:26 +00:00
naps62-yoloandClaude Opus 4.7 44e0a50192 feat(maestro): env-var overrides for registry token + bind_addr
Lets `maestro-registry` run in container deployments (e.g. Dokploy)
without mounting a writable config volume.

- `MAESTRO_TOKEN`: when set, bypasses `registry.toml` entirely (no
  read, no write). The container needs no persistent volume just to
  seed the shared secret — pass it as a platform secret instead.
- `MAESTRO_REGISTRY_BIND_ADDR`: overrides the file's `bind_addr`.
  Lets the container bind `0.0.0.0:$PORT` without rewriting TOML.
- New `load_config(path, env_token, env_bind_addr)` in `token.rs`
  centralises the env > file > default precedence. Takes explicit
  `Option`s so tests don't touch process-global env.
- `TokenError::InvalidBindAddr` carries the raw input + parse error
  so misconfigured deploys log a useful diagnostic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:12:12 +00:00
naps62-yoloandClaude Opus 4.7 3022ee2c62 feat(maestro): add maestro-registry binary entry point
ci / rust (push) Successful in 1m0s
Closes NAP-221. Adds the `maestro-registry` bin so the registry can be
spawned as a child process (required by the NAP-217 E2E harness) and
run as a long-lived service.

- New `crates/maestro-registry/src/main.rs` with a `#[tokio::main]`
  entry point: loads config via `ensure_token` (override path via
  `MAESTRO_REGISTRY_CONFIG`), binds the configured `bind_addr`, serves
  the production router, and shuts down cleanly on Ctrl-C. Mirrors the
  `maestrod` wiring (EnvFilter tracing, ExitCode reporting).
- `[lib]` + `[[bin]]` sections in `crates/maestro-registry/Cargo.toml`
  so the crate exposes both surfaces (same pattern as `maestro-daemon`).
- `RegistryConfig` gains a `bind_addr: SocketAddr` field with
  `serde(default)` to default to `127.0.0.1:8080` per spec §Registry.
  Existing TOML files without the field still load.
- `default_config_path` re-exported from the crate root for the bin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 17:28:20 +00:00
naps62-yoloandClaude Opus 4.7 18bd385a2a feat(maestro): registry HTTP API, cookie auth, JWT issuance, events WS
ci / rust (push) Successful in 48s
Adds the client-facing surface on the registry (NAP-215). Endpoints:

- GET /api/daemons — list connected daemons (with daemon_url)
- GET /api/sessions — flat session catalog
- POST /api/sessions — forwards CreateSession to chosen daemon, 504 on
  10s daemon timeout
- DELETE /api/sessions/:id — forwards KillSession
- GET /api/sessions/:id/connect-info — returns {daemon_url, jwt} with a
  5min HS256-signed attach JWT
- WS /api/events — snapshot + delta broadcast of catalog and presence
- GET / placeholder served until NAP-218 web bundle lands

Cookie auth middleware mints a long-lived httpOnly JWT cookie on first
request and reissues silently on missing/tampered cookies. Per spec the
perimeter is the operator's network — the cookie is a second factor.

Protocol additions: daemon_url on Hello, DaemonInfo, ConnectInfo,
RegistryEvent. Daemon config + RegistryClient now carry daemon_url.

Registry state now exposes an event broadcast channel (subscribe gives
a snapshot + tokio::sync::broadcast::Receiver) and helper lookups
daemon_for_session / daemon_url.

Tests: 11 reqwest-driven HTTP integration tests cover every endpoint,
the cookie flow, 504 timeout path, worktree forwarding, and events WS
propagation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 17:08:41 +00:00
naps62-yolo c721b5ec62 feat(maestro): add registry daemon-side WS handler and state
ci / rust (push) Failing after 6s
Registry-side primitives for the daemon↔registry conversation. No
client-facing HTTP yet (that's NAP-215).

- `state::Registry`: in-memory store of connected daemons + a session
  catalog keyed by (daemon_id, session_id). Tracks pending CreateSession
  / KillSession RPCs so a single daemon connection can multiplex them.
- `handler::handle_socket`: per-connection loop that authenticates the
  Hello, applies snapshot/diff/heartbeat frames, and resolves matching
  *Result frames. Heartbeat watchdog is a `tokio::time::timeout` around
  the read side — no separate janitor task.
- `server::router_with_timeout`: axum router mounting `WS /daemon`. The
  timeout knob lets tests verify the 30s drop path in milliseconds.
- `token::ensure_token`: first-run bootstrap that generates a 256-bit
  hex token, prints it once to stderr, and persists it to
  `~/.config/maestro/registry.toml`.
- Outbound `create_session` / `kill_session` exposed as the internal
  API NAP-215's HTTP handlers will call.

Integration tests use a real axum server bound to `:0` and drive it via
`tokio-tungstenite`, covering: bad-token reject, snapshot accept, diff
apply (add/update/remove), heartbeat timeout dropping the daemon,
re-registration replacing the catalog, and a full CreateSession RPC
round-trip.

Closes NAP-213.
2026-05-24 13:42:00 +00:00