Skip to content

Commit ca4b2bb

Browse files
Harden remote pairing, transfer and lifecycle recovery
1 parent c1bcf9a commit ca4b2bb

30 files changed

Lines changed: 1456 additions & 220 deletions

‎apps/maple-agent/Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎apps/maple-agent/app/Cargo.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ desktop = [
2626
"dep:spellbook",
2727
"dep:wayland-client",
2828
"dep:tray-icon",
29+
"dep:zbus",
2930
]
3031
# `maple-agent acp`: the Agent Client Protocol server on stdio.
3132
acp = ["maple-agent/acp"]
@@ -84,6 +85,7 @@ tower-http = { version = "0.6", features = ["cors"], optional = true }
8485
# `ui::decorations`. gpui cannot answer that question.
8586
[target.'cfg(target_os = "linux")'.dependencies]
8687
wayland-client = { version = "0.31", optional = true }
88+
zbus = { version = "=5.19.0", optional = true }
8789

8890
[target.'cfg(any(target_os = "linux", target_os = "windows"))'.dependencies]
8991
tray-icon = { version = "=0.26.1", default-features = false, features = ["ksni"], optional = true }

‎apps/maple-agent/app/src/backend.rs‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,6 @@ operations! {
127127
cancel_queued_message(user_id: (&str), session_id: (&str), queue_id: (&str)) -> AgentDesktopQueueSnapshot;
128128
begin_queued_message_edit(user_id: (&str), session_id: (&str), queue_id: (&str)) -> ();
129129
end_queued_message_edit(user_id: (&str), session_id: (&str), queue_id: (&str)) -> ();
130-
send_message(user_id: (&str), request: (AgentSendMessageRequest)) -> String;
131130
read_image_attachment(user_id: (&str), session_id: (&str), attachment_id: (&str)) -> Vec<u8>;
132131
cancel_run(user_id: (&str), run_id: (&str)) -> ();
133132
available_model_ids(user_id: (&str)) -> Vec<String>;
@@ -260,6 +259,27 @@ impl AgentBackend {
260259
}
261260
}
262261
}
262+
/// The composer retains this ID through uncertain delivery. Only receipt
263+
/// queries, never another send, use it after a connection loss.
264+
pub async fn send_message_identified(
265+
&self,
266+
user: &str,
267+
request: AgentSendMessageRequest,
268+
id: String,
269+
) -> Result<String, String> {
270+
match &self.client {
271+
BackendClient::Host(client) => {
272+
client
273+
.call_identified("send_message", (user, request), Some(id))
274+
.await
275+
}
276+
#[cfg(test)]
277+
BackendClient::InProcess(backend) => backend.send_message(user, request).await,
278+
}
279+
}
280+
pub async fn send_outcome(&self, id: String) -> Result<crate::host::receipts::Outcome, String> {
281+
self.call("send_outcome", id).await
282+
}
263283
pub fn runtime_handle(&self) -> tokio::runtime::Handle {
264284
self.runtime.clone()
265285
}
@@ -389,6 +409,15 @@ async fn dispatch_special(
389409
serde_json::to_value(value).map_err(|_| "Could not encode host response".to_string())
390410
};
391411
match request.method.as_str() {
412+
"send_message" => {
413+
let (user, message): (String, AgentSendMessageRequest) =
414+
serde_json::from_value(request.params)
415+
.map_err(|_| "Invalid host request parameters")?;
416+
backend.authorize_account(&user)?;
417+
serde_json::to_value(backend.send_message(&user, message).await?)
418+
.map_err(|_| "Could not encode host response".into())
419+
}
420+
392421
"restore_auth" => {
393422
serde_json::to_value(backend.validate_persisted_auth().await).map_err(|e| e.to_string())
394423
}

‎apps/maple-agent/app/src/backend/local.rs‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2689,6 +2689,60 @@ mod tests {
26892689
assert!(!remote.stays_running());
26902690
}
26912691

2692+
#[tokio::test]
2693+
async fn disabling_remote_does_not_wait_for_initializing_credentials() {
2694+
use std::{future::Future, task::Poll};
2695+
let root = tempfile::tempdir().unwrap();
2696+
let backend = Arc::new(LocalBackend::isolated_for_test(root.path()));
2697+
backend.persisted_auth.claim_and_persist(&auth_snapshot(
2698+
"fixture-user",
2699+
"fixture-session",
2700+
1,
2701+
"fixture-token",
2702+
));
2703+
let (events, _) = tokio::sync::broadcast::channel(8);
2704+
let receipts = Arc::new(
2705+
crate::host::receipts::Receipts::open(&root.path().join("receipts.db")).unwrap(),
2706+
);
2707+
let remote = crate::remote::manager::Manager::new(backend.clone(), events, receipts);
2708+
let request = |method: &str, params| crate::host::protocol::Request {
2709+
id: 1,
2710+
method: method.into(),
2711+
params,
2712+
send_id: None,
2713+
};
2714+
remote
2715+
.dispatch(request("remote_enable", serde_json::json!([true])))
2716+
.await
2717+
.unwrap();
2718+
let validation = backend.restore_lock.lock().await;
2719+
let active = remote.active();
2720+
tokio::pin!(active);
2721+
std::future::poll_fn(|cx| {
2722+
assert!(active.as_mut().poll(cx).is_pending());
2723+
Poll::Ready(())
2724+
})
2725+
.await;
2726+
tokio::time::timeout(
2727+
std::time::Duration::from_secs(1),
2728+
remote.dispatch(request("remote_enable", serde_json::json!([false]))),
2729+
)
2730+
.await
2731+
.unwrap()
2732+
.unwrap();
2733+
assert!(!remote.stays_running());
2734+
let status = remote
2735+
.dispatch(request("remote_status", serde_json::json!([])))
2736+
.await
2737+
.unwrap();
2738+
assert_eq!(status["enabled"], false);
2739+
assert_eq!(status["ready"], false);
2740+
drop(validation);
2741+
// Invalid fixture credentials cannot create a node after the cancellation.
2742+
assert!(active.await.is_err());
2743+
remote.shutdown().await;
2744+
}
2745+
26922746
#[tokio::test]
26932747
async fn credential_validation_and_acp_attachment_do_not_block_local_operations() {
26942748
use std::{future::Future, task::Poll};

0 commit comments

Comments
 (0)