feat(maestro): wire daemon to registry + per-session WS stream
Completes the headless daemon stack for NAP-214.
- `config`: daemon TOML config (daemon_id, registry_url, bind_addr,
token, state_dir, worktree_root) with sensible XDG defaults.
- `jwt`: HS256 verifier; the daemon validates per-attach JWTs locally
with no callback to the registry. Also exposes `issue()` for tests.
- `pty::PtyBridge`: per-session FIFO + tokio broadcast pump. The FIFO is
opened r+w so it never sees zero writers — stays alive across
`pipe-pane` re-issues. Helper `pipe_pane_command()` builds the
`cat >> <fifo>` shell line.
- `session::SessionManager`: authoritative live-session store. Creates
worktrees (when requested), spawns tmux, mkfifo + pipe-pane, writes
sidecar; tears it all down on kill. Emits diff events on a
broadcast for the registry client. `recover()` adopts surviving
`maestro-*` tmux sessions on startup and re-pipes them.
- `registry_client::RegistryClient`: persistent outbound WS to the
registry with exponential backoff. Sends Hello → snapshot, drives a
select loop that pumps inbound CreateSession/KillSession RPCs into
the manager (replying with results), emits SessionAdded/Removed/
Updated diffs from the manager, and heartbeats every 10s.
- `attach`: axum router mounting `WS /sessions/:id/stream?token=<jwt>`.
Binary frames carry PTY bytes either way (subscribe to broadcast →
send to client; receive from client → tmux send-keys -l). Text
frames carry `ClientControl::{Resize,Ping,Pong}`.
- `bin/maestrod`: ties everything together, loads config from
`MAESTROD_CONFIG` env or `~/.config/maestrod/config.toml`,
recovers, spawns the registry client, binds the WS server.
Also adds `JwtClaims` to `maestro-protocol` so the daemon and the
future registry JWT issuer (NAP-215) agree on the wire shape.
Integration tests cover session manager round-trip + recovery + PTY
broadcast; attach endpoint JWT (bad/wrong-session/wrong-daemon),
PTY round-trip through real tmux, multi-attach broadcast, ping/pong;
registry client registration + snapshot + diff forwarding +
CreateSession RPC round-trip.
Closes NAP-214.
This commit is contained in:
@@ -150,6 +150,24 @@ pub enum ClientControl {
|
||||
Error(ErrorInfo),
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JWT claims used on the per-session attach WS
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Issued by the registry, validated locally by the daemon. Short-lived
|
||||
/// (~5 min). The daemon checks `daemon_id` matches itself and `session_id`
|
||||
/// is one of its live sessions.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct JwtClaims {
|
||||
pub session_id: Uuid,
|
||||
pub daemon_id: String,
|
||||
/// Expiry (unix seconds).
|
||||
pub exp: i64,
|
||||
/// Issued at (unix seconds). Optional, mostly for diagnostics.
|
||||
#[serde(default)]
|
||||
pub iat: Option<i64>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -353,4 +371,23 @@ mod tests {
|
||||
let err = serde_json::from_value::<DaemonOutbound>(bad).unwrap_err();
|
||||
assert!(err.to_string().contains("Bogus"), "{}", err);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jwt_claims_round_trip_and_iat_optional() {
|
||||
let c = JwtClaims {
|
||||
session_id: Uuid::from_u128(7),
|
||||
daemon_id: "laptop".into(),
|
||||
exp: 1_700_000_000,
|
||||
iat: Some(1_699_999_700),
|
||||
};
|
||||
assert_eq!(c, round_trip(&c));
|
||||
|
||||
let no_iat = serde_json::json!({
|
||||
"session_id": Uuid::from_u128(8),
|
||||
"daemon_id": "laptop",
|
||||
"exp": 1_700_000_000_i64,
|
||||
});
|
||||
let parsed: JwtClaims = serde_json::from_value(no_iat).unwrap();
|
||||
assert!(parsed.iat.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user