feat(maestro): add ListDir/ForwardListDir wire protocol + DirListing (NAP-329)
This commit is contained in:
@@ -66,6 +66,25 @@ pub struct CreateSessionRequest {
|
|||||||
pub worktree: Option<WorktreeSpec>,
|
pub worktree: Option<WorktreeSpec>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Result of a `ListDir` query: the immediate subdirectories of one directory
|
||||||
|
/// on a daemon's host. Used to drive the web spawn form's working-directory
|
||||||
|
/// autocomplete (NAP-329).
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct DirListing {
|
||||||
|
/// The resolved absolute directory that was listed, after the daemon
|
||||||
|
/// expanded `~`/relative/empty inputs against its own home dir. The client
|
||||||
|
/// uses it to know what an abbreviated query resolved to.
|
||||||
|
pub path: PathBuf,
|
||||||
|
/// Immediate subdirectory names (not full paths), sorted case-insensitively.
|
||||||
|
/// Directories only — the spawn form's targets are always dirs. Hidden
|
||||||
|
/// entries (dotfiles) are included, matching shell completion.
|
||||||
|
pub entries: Vec<String>,
|
||||||
|
/// True when `entries` was truncated at the daemon's cap; the client can
|
||||||
|
/// surface "narrow your search" rather than implying the dir is small.
|
||||||
|
#[serde(default)]
|
||||||
|
pub truncated: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Structured failure used everywhere we don't want to leak a string-only error.
|
/// Structured failure used everywhere we don't want to leak a string-only error.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct ErrorInfo {
|
pub struct ErrorInfo {
|
||||||
@@ -158,6 +177,21 @@ pub enum DaemonOutbound {
|
|||||||
daemon_id: String,
|
daemon_id: String,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
},
|
},
|
||||||
|
/// Owning daemon's reply to a [`DaemonInbound::ListDir`] — the requested
|
||||||
|
/// directory listing on its host. (NAP-329)
|
||||||
|
ListDirResult {
|
||||||
|
reply_to: ReplyTo,
|
||||||
|
result: RpcResult<DirListing>,
|
||||||
|
},
|
||||||
|
/// Home daemon asks the registry to relay a directory listing to the daemon
|
||||||
|
/// named by `daemon_id` (which may be a peer). Control-plane only, like
|
||||||
|
/// [`ForwardCreate`](Self::ForwardCreate). Reply:
|
||||||
|
/// [`DaemonInbound::ForwardListDirResult`]. (NAP-329)
|
||||||
|
ForwardListDir {
|
||||||
|
reply_to: ReplyTo,
|
||||||
|
daemon_id: String,
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
/// Target daemon answers a [`DaemonInbound::RelaySetup`]: whether it will
|
/// Target daemon answers a [`DaemonInbound::RelaySetup`]: whether it will
|
||||||
/// accept relay traffic for the requested session. Registry only allocates
|
/// accept relay traffic for the requested session. Registry only allocates
|
||||||
/// relay capacity when `accepted == true`.
|
/// relay capacity when `accepted == true`.
|
||||||
@@ -204,6 +238,19 @@ pub enum DaemonInbound {
|
|||||||
reply_to: ReplyTo,
|
reply_to: ReplyTo,
|
||||||
result: RpcResult<()>,
|
result: RpcResult<()>,
|
||||||
},
|
},
|
||||||
|
/// Registry asks a daemon to list `path` on its host. The daemon expands
|
||||||
|
/// `~`/relative/empty against its own home dir, then replies with
|
||||||
|
/// [`DaemonOutbound::ListDirResult`]. (NAP-329)
|
||||||
|
ListDir {
|
||||||
|
reply_to: ReplyTo,
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
|
/// Registry's reply to [`DaemonOutbound::ForwardListDir`] — the owning
|
||||||
|
/// daemon's listing, relayed back to the home daemon. (NAP-329)
|
||||||
|
ForwardListDirResult {
|
||||||
|
reply_to: ReplyTo,
|
||||||
|
result: RpcResult<DirListing>,
|
||||||
|
},
|
||||||
/// Registry signals a target daemon that a home daemon wants to attach via
|
/// Registry signals a target daemon that a home daemon wants to attach via
|
||||||
/// the registry-mediated relay (T3). Target evaluates `allow_inbound` and
|
/// the registry-mediated relay (T3). Target evaluates `allow_inbound` and
|
||||||
/// replies with [`DaemonOutbound::RelayAck`].
|
/// replies with [`DaemonOutbound::RelayAck`].
|
||||||
@@ -847,6 +894,76 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dir_listing_round_trips_and_truncated_defaults() {
|
||||||
|
let listing = DirListing {
|
||||||
|
path: PathBuf::from("/home/naps62"),
|
||||||
|
entries: vec!["projects".into(), ".config".into()],
|
||||||
|
truncated: true,
|
||||||
|
};
|
||||||
|
assert_eq!(listing, round_trip(&listing));
|
||||||
|
|
||||||
|
// Older daemons omit `truncated`; it must default to false.
|
||||||
|
let json = serde_json::json!({
|
||||||
|
"path": "/home/naps62",
|
||||||
|
"entries": ["projects"]
|
||||||
|
});
|
||||||
|
let parsed: DirListing = serde_json::from_value(json).unwrap();
|
||||||
|
assert!(!parsed.truncated);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn daemon_list_dir_round_trips_and_tag() {
|
||||||
|
let req = DaemonInbound::ListDir {
|
||||||
|
reply_to: Uuid::from_u128(61),
|
||||||
|
path: "~/projects".into(),
|
||||||
|
};
|
||||||
|
let v = serde_json::to_value(&req).unwrap();
|
||||||
|
assert_eq!(v["type"], "ListDir");
|
||||||
|
assert_eq!(v["path"], "~/projects");
|
||||||
|
assert_eq!(req, round_trip(&req));
|
||||||
|
|
||||||
|
let res = DaemonOutbound::ListDirResult {
|
||||||
|
reply_to: Uuid::from_u128(61),
|
||||||
|
result: RpcResult::Ok {
|
||||||
|
value: DirListing {
|
||||||
|
path: PathBuf::from("/home/naps62/projects"),
|
||||||
|
entries: vec!["maestro".into()],
|
||||||
|
truncated: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let v = serde_json::to_value(&res).unwrap();
|
||||||
|
assert_eq!(v["type"], "ListDirResult");
|
||||||
|
assert_eq!(res, round_trip(&res));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn daemon_forward_list_dir_round_trips_and_tag() {
|
||||||
|
let fwd = DaemonOutbound::ForwardListDir {
|
||||||
|
reply_to: Uuid::from_u128(62),
|
||||||
|
daemon_id: "konishi".into(),
|
||||||
|
path: "/work".into(),
|
||||||
|
};
|
||||||
|
let v = serde_json::to_value(&fwd).unwrap();
|
||||||
|
assert_eq!(v["type"], "ForwardListDir");
|
||||||
|
assert_eq!(v["daemon_id"], "konishi");
|
||||||
|
assert_eq!(fwd, round_trip(&fwd));
|
||||||
|
|
||||||
|
let res = DaemonInbound::ForwardListDirResult {
|
||||||
|
reply_to: Uuid::from_u128(62),
|
||||||
|
result: RpcResult::Err {
|
||||||
|
error: ErrorInfo {
|
||||||
|
code: "permission_denied".into(),
|
||||||
|
message: "EACCES".into(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let v = serde_json::to_value(&res).unwrap();
|
||||||
|
assert_eq!(v["type"], "ForwardListDirResult");
|
||||||
|
assert_eq!(res, round_trip(&res));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn daemon_outbound_relay_ack_round_trip_and_tag() {
|
fn daemon_outbound_relay_ack_round_trip_and_tag() {
|
||||||
let msg = DaemonOutbound::RelayAck {
|
let msg = DaemonOutbound::RelayAck {
|
||||||
|
|||||||
Reference in New Issue
Block a user