feat(maestro): add ListDir/ForwardListDir wire protocol + DirListing (NAP-329)

This commit is contained in:
2026-06-15 21:11:43 +00:00
parent 8842147164
commit 1f20d70a2e
+117
View File
@@ -66,6 +66,25 @@ pub struct CreateSessionRequest {
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.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ErrorInfo {
@@ -158,6 +177,21 @@ pub enum DaemonOutbound {
daemon_id: String,
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
/// accept relay traffic for the requested session. Registry only allocates
/// relay capacity when `accepted == true`.
@@ -204,6 +238,19 @@ pub enum DaemonInbound {
reply_to: ReplyTo,
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
/// the registry-mediated relay (T3). Target evaluates `allow_inbound` and
/// 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]
fn daemon_outbound_relay_ack_round_trip_and_tag() {
let msg = DaemonOutbound::RelayAck {