reallink-cli 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -3
- package/prebuilt/linux-x64/reallink-cli +0 -0
- package/rust/Cargo.lock +2860 -256
- package/rust/Cargo.toml +5 -2
- package/rust/src/main.rs +806 -11
- package/rust/src/unreal.rs +50 -0
package/rust/src/main.rs
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
use anyhow::{anyhow, Context, Result};
|
|
2
|
+
use axum::extract::State;
|
|
3
|
+
use axum::http::StatusCode as AxumStatusCode;
|
|
4
|
+
use axum::routing::{get, post};
|
|
5
|
+
use axum::{Json, Router};
|
|
6
|
+
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
|
7
|
+
use base64::Engine;
|
|
2
8
|
use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
|
|
9
|
+
use iroh::{endpoint::presets, Endpoint, EndpointAddr};
|
|
3
10
|
use regex::RegexBuilder;
|
|
4
11
|
use reqwest::{Method, StatusCode};
|
|
5
12
|
use serde::{Deserialize, Serialize};
|
|
6
13
|
use sha2::Digest;
|
|
7
14
|
use std::fs;
|
|
8
15
|
use std::io::{self, Read, SeekFrom, Write};
|
|
16
|
+
use std::net::SocketAddr;
|
|
9
17
|
use std::path::{Path, PathBuf};
|
|
10
18
|
use std::process::Command;
|
|
19
|
+
use std::sync::Arc;
|
|
11
20
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
12
21
|
use tokio::fs as tokio_fs;
|
|
13
22
|
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncSeekExt, AsyncWriteExt, BufReader};
|
|
23
|
+
use tokio::sync::RwLock;
|
|
14
24
|
use tokio::time::sleep;
|
|
15
25
|
|
|
16
26
|
mod generated;
|
|
@@ -18,7 +28,7 @@ mod logs;
|
|
|
18
28
|
mod unreal;
|
|
19
29
|
use unreal::{
|
|
20
30
|
LinkConnectArgs, LinkDoctorArgs, LinkOpenArgs, LinkP2PCreateArgs, LinkP2PGetArgs,
|
|
21
|
-
LinkP2PListArgs, LinkP2PSignalArgs, LinkP2PWaitArgs, LinkPathsArgs, LinkPluginInstallArgs,
|
|
31
|
+
LinkP2PExecuteArgs, LinkP2PListArgs, LinkP2PSignalArgs, LinkP2PWaitArgs, LinkPathsArgs, LinkPluginInstallArgs,
|
|
22
32
|
LinkPluginListArgs, LinkRemoveArgs, LinkRunArgs, LinkSourceArgs, LinkUnrealArgs, LinkUseArgs,
|
|
23
33
|
PluginIndexFile, SourceLinkRecord, SourceLinksConfig, UnrealLinkRecord, UnrealLinksConfig,
|
|
24
34
|
};
|
|
@@ -31,6 +41,11 @@ const UNREAL_LINKS_FILE_NAME: &str = "unreal-links.json";
|
|
|
31
41
|
const UPDATE_CACHE_FILE_NAME: &str = "update-check.json";
|
|
32
42
|
const VERSION_CHECK_INTERVAL_MS: u128 = 24 * 60 * 60 * 1000;
|
|
33
43
|
const DEFAULT_VERSION_FETCH_TIMEOUT_MS: u64 = 800;
|
|
44
|
+
const P2P_EXECUTE_SESSION_FETCH_ATTEMPTS: usize = 3;
|
|
45
|
+
const P2P_EXECUTE_ENDPOINT_ATTEMPTS: usize = 3;
|
|
46
|
+
const P2P_EXECUTE_RETRY_DELAY_MS: u64 = 350;
|
|
47
|
+
const P2P_IROH_ALPN: &[u8] = b"reallink-p2p-direct-v1";
|
|
48
|
+
const P2P_IROH_MAX_MESSAGE_BYTES: usize = 1024 * 1024;
|
|
34
49
|
|
|
35
50
|
#[derive(Parser)]
|
|
36
51
|
#[command(
|
|
@@ -7209,6 +7224,34 @@ pub(crate) async fn link_connect_command(
|
|
|
7209
7224
|
args.transport.trim().to_lowercase()
|
|
7210
7225
|
)
|
|
7211
7226
|
});
|
|
7227
|
+
let registered_client_id = Arc::new(RwLock::new(None::<String>));
|
|
7228
|
+
let p2p_iroh_endpoint = if args.transport.trim().eq_ignore_ascii_case("p2p") {
|
|
7229
|
+
Some(create_p2p_iroh_endpoint().await?)
|
|
7230
|
+
} else {
|
|
7231
|
+
None
|
|
7232
|
+
};
|
|
7233
|
+
let p2p_iroh_addr_json = if let Some(endpoint) = &p2p_iroh_endpoint {
|
|
7234
|
+
Some(serde_json::to_string(&endpoint.addr()).context("encode p2p iroh addr")?)
|
|
7235
|
+
} else {
|
|
7236
|
+
None
|
|
7237
|
+
};
|
|
7238
|
+
let p2p_direct_servers = if args.transport.trim().eq_ignore_ascii_case("p2p") {
|
|
7239
|
+
Some(
|
|
7240
|
+
spawn_p2p_direct_server(
|
|
7241
|
+
&args.p2p_listen,
|
|
7242
|
+
&args.p2p_tcp_listen,
|
|
7243
|
+
p2p_iroh_endpoint.clone().expect("missing p2p iroh endpoint"),
|
|
7244
|
+
Arc::new(allowed_roots.clone()),
|
|
7245
|
+
registered_client_id.clone(),
|
|
7246
|
+
project_id.clone(),
|
|
7247
|
+
base_url.to_string(),
|
|
7248
|
+
access_token.to_string(),
|
|
7249
|
+
)
|
|
7250
|
+
.await?,
|
|
7251
|
+
)
|
|
7252
|
+
} else {
|
|
7253
|
+
None
|
|
7254
|
+
};
|
|
7212
7255
|
|
|
7213
7256
|
// Reconnect loop with exponential backoff
|
|
7214
7257
|
let mut backoff_ms: u64 = 1000;
|
|
@@ -7264,6 +7307,8 @@ pub(crate) async fn link_connect_command(
|
|
|
7264
7307
|
"capabilities": ["stat", "read", "readRange", "readLines", "list", "search", "write", "mkdir", "delete"],
|
|
7265
7308
|
"metadata": {
|
|
7266
7309
|
"cliVersion": env!("CARGO_PKG_VERSION"),
|
|
7310
|
+
"irohAddrJson": p2p_iroh_addr_json,
|
|
7311
|
+
"p2pCarrier": if args.transport.trim().eq_ignore_ascii_case("p2p") { Some("iroh-quic-v1") } else { None },
|
|
7267
7312
|
}
|
|
7268
7313
|
});
|
|
7269
7314
|
if let Err(e) = ws_stream
|
|
@@ -7287,7 +7332,7 @@ pub(crate) async fn link_connect_command(
|
|
|
7287
7332
|
msg_opt = ws_stream.next() => {
|
|
7288
7333
|
match msg_opt {
|
|
7289
7334
|
Some(Ok(Message::Text(text))) => {
|
|
7290
|
-
if let Err(e) = handle_ws_message(&text, &allowed_roots, &mut ws_stream).await {
|
|
7335
|
+
if let Err(e) = handle_ws_message(&text, &allowed_roots, ®istered_client_id, &mut ws_stream).await {
|
|
7291
7336
|
eprintln!("Error handling message: {}", e);
|
|
7292
7337
|
}
|
|
7293
7338
|
},
|
|
@@ -7311,6 +7356,11 @@ pub(crate) async fn link_connect_command(
|
|
|
7311
7356
|
},
|
|
7312
7357
|
_ = tokio::signal::ctrl_c() => {
|
|
7313
7358
|
eprintln!("\nDisconnecting...");
|
|
7359
|
+
if let Some(servers) = &p2p_direct_servers {
|
|
7360
|
+
for server in servers {
|
|
7361
|
+
server.abort();
|
|
7362
|
+
}
|
|
7363
|
+
}
|
|
7314
7364
|
let _ = ws_stream.close(None).await;
|
|
7315
7365
|
return Ok(());
|
|
7316
7366
|
}
|
|
@@ -7539,9 +7589,711 @@ pub(crate) async fn link_p2p_signal_command(
|
|
|
7539
7589
|
Ok(())
|
|
7540
7590
|
}
|
|
7541
7591
|
|
|
7592
|
+
#[derive(Clone)]
|
|
7593
|
+
struct P2PDirectServerState {
|
|
7594
|
+
allowed_roots: Arc<Vec<std::path::PathBuf>>,
|
|
7595
|
+
client_id: Arc<RwLock<Option<String>>>,
|
|
7596
|
+
project_id: String,
|
|
7597
|
+
base_url: String,
|
|
7598
|
+
access_token: String,
|
|
7599
|
+
}
|
|
7600
|
+
|
|
7601
|
+
#[derive(Debug, Deserialize)]
|
|
7602
|
+
#[serde(rename_all = "camelCase")]
|
|
7603
|
+
struct P2PDirectExecuteRequest {
|
|
7604
|
+
session_id: String,
|
|
7605
|
+
tool: String,
|
|
7606
|
+
#[serde(default)]
|
|
7607
|
+
input: serde_json::Value,
|
|
7608
|
+
}
|
|
7609
|
+
|
|
7610
|
+
#[derive(Debug, Deserialize)]
|
|
7611
|
+
#[serde(rename_all = "camelCase")]
|
|
7612
|
+
struct P2PSessionEnvelope {
|
|
7613
|
+
session: P2PSessionRecord,
|
|
7614
|
+
}
|
|
7615
|
+
|
|
7616
|
+
#[derive(Debug, Deserialize)]
|
|
7617
|
+
#[serde(rename_all = "camelCase")]
|
|
7618
|
+
struct P2PSessionRecord {
|
|
7619
|
+
session_id: String,
|
|
7620
|
+
status: String,
|
|
7621
|
+
signals: Vec<P2PSignalRecord>,
|
|
7622
|
+
}
|
|
7623
|
+
|
|
7624
|
+
#[derive(Debug, Deserialize)]
|
|
7625
|
+
#[serde(rename_all = "camelCase")]
|
|
7626
|
+
struct P2PSignalRecord {
|
|
7627
|
+
role: String,
|
|
7628
|
+
signal_type: String,
|
|
7629
|
+
from_client_id: Option<String>,
|
|
7630
|
+
payload: serde_json::Value,
|
|
7631
|
+
}
|
|
7632
|
+
|
|
7633
|
+
#[derive(Debug, Serialize)]
|
|
7634
|
+
#[serde(rename_all = "camelCase")]
|
|
7635
|
+
struct P2PDirectPeerInfo {
|
|
7636
|
+
endpoint: String,
|
|
7637
|
+
iroh_addr_json: Option<String>,
|
|
7638
|
+
role: String,
|
|
7639
|
+
from_client_id: Option<String>,
|
|
7640
|
+
}
|
|
7641
|
+
|
|
7642
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
7643
|
+
#[serde(rename_all = "camelCase")]
|
|
7644
|
+
struct P2PDirectTcpEnvelope {
|
|
7645
|
+
session_id: String,
|
|
7646
|
+
tool: String,
|
|
7647
|
+
#[serde(default)]
|
|
7648
|
+
input: serde_json::Value,
|
|
7649
|
+
auth_token: String,
|
|
7650
|
+
}
|
|
7651
|
+
|
|
7652
|
+
fn value_as_trimmed_string(value: Option<&serde_json::Value>) -> Option<String> {
|
|
7653
|
+
value.and_then(|item| item.as_str()).map(|item| item.trim().to_string()).filter(|item| !item.is_empty())
|
|
7654
|
+
}
|
|
7655
|
+
|
|
7656
|
+
async fn fetch_p2p_session(
|
|
7657
|
+
client: &reqwest::Client,
|
|
7658
|
+
base_url: &str,
|
|
7659
|
+
access_token: &str,
|
|
7660
|
+
project_id: &str,
|
|
7661
|
+
session_id: &str,
|
|
7662
|
+
) -> Result<P2PSessionRecord> {
|
|
7663
|
+
let endpoint = format!(
|
|
7664
|
+
"{}/v1/projects/{}/connect/p2p/sessions/{}",
|
|
7665
|
+
base_url.trim_end_matches('/'),
|
|
7666
|
+
urlencoding::encode(project_id),
|
|
7667
|
+
urlencoding::encode(session_id)
|
|
7668
|
+
);
|
|
7669
|
+
let response = client
|
|
7670
|
+
.get(endpoint)
|
|
7671
|
+
.bearer_auth(access_token)
|
|
7672
|
+
.send()
|
|
7673
|
+
.await
|
|
7674
|
+
.context("fetch p2p session")?;
|
|
7675
|
+
let status = response.status();
|
|
7676
|
+
let payload: serde_json::Value = response.json().await.unwrap_or_else(|_| serde_json::json!({}));
|
|
7677
|
+
if !status.is_success() {
|
|
7678
|
+
return Err(anyhow!(
|
|
7679
|
+
"fetch p2p session failed with status {}: {}",
|
|
7680
|
+
status,
|
|
7681
|
+
payload
|
|
7682
|
+
));
|
|
7683
|
+
}
|
|
7684
|
+
let parsed: P2PSessionEnvelope = serde_json::from_value(payload)
|
|
7685
|
+
.context("parse p2p session response")?;
|
|
7686
|
+
Ok(parsed.session)
|
|
7687
|
+
}
|
|
7688
|
+
|
|
7689
|
+
fn p2p_should_retry_status(status: StatusCode) -> bool {
|
|
7690
|
+
matches!(
|
|
7691
|
+
status,
|
|
7692
|
+
StatusCode::REQUEST_TIMEOUT
|
|
7693
|
+
| StatusCode::TOO_MANY_REQUESTS
|
|
7694
|
+
| StatusCode::BAD_GATEWAY
|
|
7695
|
+
| StatusCode::SERVICE_UNAVAILABLE
|
|
7696
|
+
| StatusCode::GATEWAY_TIMEOUT
|
|
7697
|
+
) || status.is_server_error()
|
|
7698
|
+
}
|
|
7699
|
+
|
|
7700
|
+
fn resolve_target_signal(
|
|
7701
|
+
session: &P2PSessionRecord,
|
|
7702
|
+
target_role: &str,
|
|
7703
|
+
) -> Result<(P2PSignalRecord, P2PDirectPeerInfo, String)> {
|
|
7704
|
+
for signal in session.signals.iter().rev() {
|
|
7705
|
+
if signal.role != target_role {
|
|
7706
|
+
continue;
|
|
7707
|
+
}
|
|
7708
|
+
let endpoint = value_as_trimmed_string(signal.payload.get("endpoint"))
|
|
7709
|
+
.ok_or_else(|| anyhow!("target {} signal missing endpoint", target_role))?;
|
|
7710
|
+
let iroh_addr_json = value_as_trimmed_string(signal.payload.get("irohAddrJson"));
|
|
7711
|
+
let auth_token = value_as_trimmed_string(signal.payload.get("authToken"))
|
|
7712
|
+
.ok_or_else(|| anyhow!("target {} signal missing authToken", target_role))?;
|
|
7713
|
+
return Ok((
|
|
7714
|
+
P2PSignalRecord {
|
|
7715
|
+
role: signal.role.clone(),
|
|
7716
|
+
signal_type: signal.signal_type.clone(),
|
|
7717
|
+
from_client_id: signal.from_client_id.clone(),
|
|
7718
|
+
payload: signal.payload.clone(),
|
|
7719
|
+
},
|
|
7720
|
+
P2PDirectPeerInfo {
|
|
7721
|
+
endpoint,
|
|
7722
|
+
iroh_addr_json,
|
|
7723
|
+
role: signal.role.clone(),
|
|
7724
|
+
from_client_id: signal.from_client_id.clone(),
|
|
7725
|
+
},
|
|
7726
|
+
auth_token,
|
|
7727
|
+
));
|
|
7728
|
+
}
|
|
7729
|
+
Err(anyhow!("no target {} signal with endpoint/authToken found", target_role))
|
|
7730
|
+
}
|
|
7731
|
+
|
|
7732
|
+
fn candidate_peer_endpoints(endpoint: &str) -> Vec<String> {
|
|
7733
|
+
let mut endpoints = vec![endpoint.to_string()];
|
|
7734
|
+
if endpoint.contains("host.docker.internal") {
|
|
7735
|
+
endpoints.push(endpoint.replace("host.docker.internal", "127.0.0.1"));
|
|
7736
|
+
endpoints.push(endpoint.replace("host.docker.internal", "localhost"));
|
|
7737
|
+
}
|
|
7738
|
+
endpoints.dedup();
|
|
7739
|
+
endpoints
|
|
7740
|
+
}
|
|
7741
|
+
|
|
7742
|
+
async fn invoke_p2p_http_endpoint(
|
|
7743
|
+
client: &reqwest::Client,
|
|
7744
|
+
endpoint: &str,
|
|
7745
|
+
auth_token: &str,
|
|
7746
|
+
session_id: &str,
|
|
7747
|
+
tool: &str,
|
|
7748
|
+
input: &serde_json::Value,
|
|
7749
|
+
) -> Result<(StatusCode, serde_json::Value)> {
|
|
7750
|
+
let response = client
|
|
7751
|
+
.post(endpoint.to_string())
|
|
7752
|
+
.header("x-reallink-p2p-token", auth_token)
|
|
7753
|
+
.json(&serde_json::json!({
|
|
7754
|
+
"sessionId": session_id,
|
|
7755
|
+
"tool": tool,
|
|
7756
|
+
"input": input,
|
|
7757
|
+
}))
|
|
7758
|
+
.send()
|
|
7759
|
+
.await?;
|
|
7760
|
+
let status = response.status();
|
|
7761
|
+
let payload = response.json().await.unwrap_or_else(|_| serde_json::json!({}));
|
|
7762
|
+
Ok((status, payload))
|
|
7763
|
+
}
|
|
7764
|
+
|
|
7765
|
+
async fn invoke_p2p_tcp_endpoint(
|
|
7766
|
+
endpoint: &str,
|
|
7767
|
+
auth_token: &str,
|
|
7768
|
+
session_id: &str,
|
|
7769
|
+
tool: &str,
|
|
7770
|
+
input: &serde_json::Value,
|
|
7771
|
+
) -> Result<(StatusCode, serde_json::Value)> {
|
|
7772
|
+
let tcp_target = endpoint.trim_start_matches("tcp://");
|
|
7773
|
+
let mut stream = tokio::net::TcpStream::connect(tcp_target).await
|
|
7774
|
+
.with_context(|| format!("connect tcp p2p endpoint {}", endpoint))?;
|
|
7775
|
+
let envelope = serde_json::to_string(&P2PDirectTcpEnvelope {
|
|
7776
|
+
session_id: session_id.to_string(),
|
|
7777
|
+
tool: tool.to_string(),
|
|
7778
|
+
input: input.clone(),
|
|
7779
|
+
auth_token: auth_token.to_string(),
|
|
7780
|
+
})?;
|
|
7781
|
+
stream.write_all(envelope.as_bytes()).await?;
|
|
7782
|
+
stream.write_all(b"\n").await?;
|
|
7783
|
+
let mut reader = BufReader::new(stream);
|
|
7784
|
+
let mut line = String::new();
|
|
7785
|
+
reader.read_line(&mut line).await?;
|
|
7786
|
+
let payload: serde_json::Value = serde_json::from_str(line.trim())
|
|
7787
|
+
.unwrap_or_else(|_| serde_json::json!({ "ok": false, "error": "invalid tcp response" }));
|
|
7788
|
+
let status = if payload.get("ok").and_then(|value| value.as_bool()).unwrap_or(false) {
|
|
7789
|
+
StatusCode::OK
|
|
7790
|
+
} else {
|
|
7791
|
+
StatusCode::BAD_GATEWAY
|
|
7792
|
+
};
|
|
7793
|
+
Ok((status, payload))
|
|
7794
|
+
}
|
|
7795
|
+
|
|
7796
|
+
fn parse_iroh_addr(addr_json: &str) -> Result<EndpointAddr> {
|
|
7797
|
+
serde_json::from_str(addr_json).context("parse iroh endpoint addr")
|
|
7798
|
+
}
|
|
7799
|
+
|
|
7800
|
+
async fn create_p2p_iroh_endpoint() -> Result<Endpoint> {
|
|
7801
|
+
let endpoint = Endpoint::builder(presets::N0)
|
|
7802
|
+
.alpns(vec![P2P_IROH_ALPN.to_vec()])
|
|
7803
|
+
.bind()
|
|
7804
|
+
.await
|
|
7805
|
+
.context("bind iroh endpoint")?;
|
|
7806
|
+
endpoint.online().await;
|
|
7807
|
+
Ok(endpoint)
|
|
7808
|
+
}
|
|
7809
|
+
|
|
7810
|
+
async fn invoke_p2p_iroh_endpoint(
|
|
7811
|
+
iroh_addr_json: &str,
|
|
7812
|
+
auth_token: &str,
|
|
7813
|
+
session_id: &str,
|
|
7814
|
+
tool: &str,
|
|
7815
|
+
input: &serde_json::Value,
|
|
7816
|
+
) -> Result<(StatusCode, serde_json::Value)> {
|
|
7817
|
+
let endpoint_addr = parse_iroh_addr(iroh_addr_json)?;
|
|
7818
|
+
let endpoint = create_p2p_iroh_endpoint().await?;
|
|
7819
|
+
let connection = endpoint
|
|
7820
|
+
.connect(endpoint_addr, P2P_IROH_ALPN)
|
|
7821
|
+
.await
|
|
7822
|
+
.context("connect iroh endpoint")?;
|
|
7823
|
+
let (mut send, mut recv) = connection.open_bi().await.context("open iroh request stream")?;
|
|
7824
|
+
let envelope = serde_json::to_vec(&P2PDirectTcpEnvelope {
|
|
7825
|
+
session_id: session_id.to_string(),
|
|
7826
|
+
tool: tool.to_string(),
|
|
7827
|
+
input: input.clone(),
|
|
7828
|
+
auth_token: auth_token.to_string(),
|
|
7829
|
+
})?;
|
|
7830
|
+
send.write_u32(envelope.len() as u32)
|
|
7831
|
+
.await
|
|
7832
|
+
.context("write iroh request length")?;
|
|
7833
|
+
send.write_all(&envelope).await.context("write iroh request")?;
|
|
7834
|
+
send.flush().await.context("flush iroh request")?;
|
|
7835
|
+
send.finish().context("finish iroh request stream")?;
|
|
7836
|
+
let response_len = recv
|
|
7837
|
+
.read_u32()
|
|
7838
|
+
.await
|
|
7839
|
+
.context("read iroh response length")? as usize;
|
|
7840
|
+
if response_len > P2P_IROH_MAX_MESSAGE_BYTES {
|
|
7841
|
+
return Err(anyhow!(
|
|
7842
|
+
"iroh response too large: {} bytes exceeds limit {}",
|
|
7843
|
+
response_len,
|
|
7844
|
+
P2P_IROH_MAX_MESSAGE_BYTES
|
|
7845
|
+
));
|
|
7846
|
+
}
|
|
7847
|
+
let mut response_bytes = vec![0; response_len];
|
|
7848
|
+
recv.read_exact(&mut response_bytes)
|
|
7849
|
+
.await
|
|
7850
|
+
.map_err(|err| anyhow!("read iroh response: {:#}", err))?;
|
|
7851
|
+
let payload: serde_json::Value = serde_json::from_slice(&response_bytes)
|
|
7852
|
+
.unwrap_or_else(|_| serde_json::json!({ "ok": false, "error": "invalid iroh response" }));
|
|
7853
|
+
let status = if payload.get("ok").and_then(|value| value.as_bool()).unwrap_or(false) {
|
|
7854
|
+
StatusCode::OK
|
|
7855
|
+
} else {
|
|
7856
|
+
StatusCode::BAD_GATEWAY
|
|
7857
|
+
};
|
|
7858
|
+
connection.close(0u32.into(), b"done");
|
|
7859
|
+
endpoint.close().await;
|
|
7860
|
+
Ok((status, payload))
|
|
7861
|
+
}
|
|
7862
|
+
|
|
7863
|
+
async fn p2p_direct_health() -> Json<serde_json::Value> {
|
|
7864
|
+
Json(serde_json::json!({ "ok": true }))
|
|
7865
|
+
}
|
|
7866
|
+
|
|
7867
|
+
async fn p2p_direct_execute(
|
|
7868
|
+
State(state): State<P2PDirectServerState>,
|
|
7869
|
+
headers: axum::http::HeaderMap,
|
|
7870
|
+
Json(payload): Json<P2PDirectExecuteRequest>,
|
|
7871
|
+
) -> Result<Json<serde_json::Value>, (AxumStatusCode, Json<serde_json::Value>)> {
|
|
7872
|
+
let presented_token = headers
|
|
7873
|
+
.get("x-reallink-p2p-token")
|
|
7874
|
+
.and_then(|value| value.to_str().ok())
|
|
7875
|
+
.map(|value| value.trim().to_string())
|
|
7876
|
+
.filter(|value| !value.is_empty())
|
|
7877
|
+
.ok_or_else(|| {
|
|
7878
|
+
(
|
|
7879
|
+
AxumStatusCode::UNAUTHORIZED,
|
|
7880
|
+
Json(serde_json::json!({ "ok": false, "error": "missing p2p token" })),
|
|
7881
|
+
)
|
|
7882
|
+
})?;
|
|
7883
|
+
|
|
7884
|
+
let registered_client_id = state.client_id.read().await.clone().ok_or_else(|| {
|
|
7885
|
+
(
|
|
7886
|
+
AxumStatusCode::SERVICE_UNAVAILABLE,
|
|
7887
|
+
Json(serde_json::json!({ "ok": false, "error": "p2p client not registered yet" })),
|
|
7888
|
+
)
|
|
7889
|
+
})?;
|
|
7890
|
+
|
|
7891
|
+
let http_client = reqwest::Client::new();
|
|
7892
|
+
let session = fetch_p2p_session(
|
|
7893
|
+
&http_client,
|
|
7894
|
+
&state.base_url,
|
|
7895
|
+
&state.access_token,
|
|
7896
|
+
&state.project_id,
|
|
7897
|
+
&payload.session_id,
|
|
7898
|
+
)
|
|
7899
|
+
.await
|
|
7900
|
+
.map_err(|err| {
|
|
7901
|
+
(
|
|
7902
|
+
AxumStatusCode::BAD_GATEWAY,
|
|
7903
|
+
Json(serde_json::json!({ "ok": false, "error": err.to_string() })),
|
|
7904
|
+
)
|
|
7905
|
+
})?;
|
|
7906
|
+
|
|
7907
|
+
if session.status != "ready" {
|
|
7908
|
+
return Err((
|
|
7909
|
+
AxumStatusCode::CONFLICT,
|
|
7910
|
+
Json(serde_json::json!({ "ok": false, "error": format!("p2p session {} not ready", session.session_id) })),
|
|
7911
|
+
));
|
|
7912
|
+
}
|
|
7913
|
+
|
|
7914
|
+
let authorized = session.signals.iter().rev().find(|signal| {
|
|
7915
|
+
signal.from_client_id.as_deref() == Some(registered_client_id.as_str())
|
|
7916
|
+
&& value_as_trimmed_string(signal.payload.get("authToken")).as_deref() == Some(presented_token.as_str())
|
|
7917
|
+
});
|
|
7918
|
+
if authorized.is_none() {
|
|
7919
|
+
return Err((
|
|
7920
|
+
AxumStatusCode::UNAUTHORIZED,
|
|
7921
|
+
Json(serde_json::json!({ "ok": false, "error": "p2p token not authorized for this client/session" })),
|
|
7922
|
+
));
|
|
7923
|
+
}
|
|
7924
|
+
|
|
7925
|
+
let result = execute_local_tool(&payload.tool, &payload.input, state.allowed_roots.as_slice()).await;
|
|
7926
|
+
Ok(Json(result))
|
|
7927
|
+
}
|
|
7928
|
+
|
|
7929
|
+
async fn execute_p2p_direct_request(
|
|
7930
|
+
state: &P2PDirectServerState,
|
|
7931
|
+
session_id: &str,
|
|
7932
|
+
auth_token: &str,
|
|
7933
|
+
tool: &str,
|
|
7934
|
+
input: &serde_json::Value,
|
|
7935
|
+
) -> Result<serde_json::Value> {
|
|
7936
|
+
let registered_client_id = state.client_id.read().await.clone()
|
|
7937
|
+
.ok_or_else(|| anyhow!("p2p client not registered yet"))?;
|
|
7938
|
+
let http_client = reqwest::Client::new();
|
|
7939
|
+
let session = fetch_p2p_session(
|
|
7940
|
+
&http_client,
|
|
7941
|
+
&state.base_url,
|
|
7942
|
+
&state.access_token,
|
|
7943
|
+
&state.project_id,
|
|
7944
|
+
session_id,
|
|
7945
|
+
)
|
|
7946
|
+
.await?;
|
|
7947
|
+
if session.status != "ready" {
|
|
7948
|
+
return Err(anyhow!("p2p session {} not ready", session.session_id));
|
|
7949
|
+
}
|
|
7950
|
+
let authorized = session.signals.iter().rev().find(|signal| {
|
|
7951
|
+
signal.from_client_id.as_deref() == Some(registered_client_id.as_str())
|
|
7952
|
+
&& value_as_trimmed_string(signal.payload.get("authToken")).as_deref() == Some(auth_token)
|
|
7953
|
+
});
|
|
7954
|
+
if authorized.is_none() {
|
|
7955
|
+
return Err(anyhow!("p2p token not authorized for this client/session"));
|
|
7956
|
+
}
|
|
7957
|
+
Ok(execute_local_tool(tool, input, state.allowed_roots.as_slice()).await)
|
|
7958
|
+
}
|
|
7959
|
+
|
|
7960
|
+
async fn spawn_p2p_tcp_server(
|
|
7961
|
+
listen_addr: &str,
|
|
7962
|
+
state: P2PDirectServerState,
|
|
7963
|
+
) -> Result<tokio::task::JoinHandle<()>> {
|
|
7964
|
+
let listener = tokio::net::TcpListener::bind(listen_addr)
|
|
7965
|
+
.await
|
|
7966
|
+
.with_context(|| format!("bind p2p tcp listen address {}", listen_addr))?;
|
|
7967
|
+
let local_addr: SocketAddr = listener.local_addr().context("resolve p2p tcp local addr")?;
|
|
7968
|
+
eprintln!("P2P TCP server listening on tcp://{}", local_addr);
|
|
7969
|
+
let shared = Arc::new(state);
|
|
7970
|
+
Ok(tokio::spawn(async move {
|
|
7971
|
+
loop {
|
|
7972
|
+
let (stream, _peer_addr) = match listener.accept().await {
|
|
7973
|
+
Ok(value) => value,
|
|
7974
|
+
Err(err) => {
|
|
7975
|
+
eprintln!("P2P TCP accept failed: {}", err);
|
|
7976
|
+
continue;
|
|
7977
|
+
}
|
|
7978
|
+
};
|
|
7979
|
+
let state = shared.clone();
|
|
7980
|
+
tokio::spawn(async move {
|
|
7981
|
+
let (reader, mut writer) = stream.into_split();
|
|
7982
|
+
let mut reader = BufReader::new(reader);
|
|
7983
|
+
let mut line = String::new();
|
|
7984
|
+
let response = match reader.read_line(&mut line).await {
|
|
7985
|
+
Ok(0) => serde_json::json!({ "ok": false, "error": "empty tcp request" }),
|
|
7986
|
+
Ok(_) => match serde_json::from_str::<P2PDirectTcpEnvelope>(line.trim()) {
|
|
7987
|
+
Ok(payload) => match execute_p2p_direct_request(
|
|
7988
|
+
&state,
|
|
7989
|
+
&payload.session_id,
|
|
7990
|
+
&payload.auth_token,
|
|
7991
|
+
&payload.tool,
|
|
7992
|
+
&payload.input,
|
|
7993
|
+
)
|
|
7994
|
+
.await
|
|
7995
|
+
{
|
|
7996
|
+
Ok(result) => result,
|
|
7997
|
+
Err(err) => serde_json::json!({ "ok": false, "error": err.to_string() }),
|
|
7998
|
+
},
|
|
7999
|
+
Err(err) => serde_json::json!({ "ok": false, "error": format!("invalid tcp request: {}", err) }),
|
|
8000
|
+
},
|
|
8001
|
+
Err(err) => serde_json::json!({ "ok": false, "error": format!("tcp read failed: {}", err) }),
|
|
8002
|
+
};
|
|
8003
|
+
let payload = match serde_json::to_string(&response) {
|
|
8004
|
+
Ok(value) => value,
|
|
8005
|
+
Err(err) => serde_json::json!({ "ok": false, "error": format!("tcp response encode failed: {}", err) }).to_string(),
|
|
8006
|
+
};
|
|
8007
|
+
let _ = writer.write_all(payload.as_bytes()).await;
|
|
8008
|
+
let _ = writer.write_all(b"\n").await;
|
|
8009
|
+
let _ = writer.shutdown().await;
|
|
8010
|
+
});
|
|
8011
|
+
}
|
|
8012
|
+
}))
|
|
8013
|
+
}
|
|
8014
|
+
|
|
8015
|
+
async fn spawn_p2p_iroh_server(
|
|
8016
|
+
endpoint: Endpoint,
|
|
8017
|
+
state: P2PDirectServerState,
|
|
8018
|
+
) -> Result<tokio::task::JoinHandle<()>> {
|
|
8019
|
+
let local_addr_json = serde_json::to_string(&endpoint.addr()).context("encode iroh endpoint addr")?;
|
|
8020
|
+
eprintln!("P2P iroh server online: {}", local_addr_json);
|
|
8021
|
+
let shared = Arc::new(state);
|
|
8022
|
+
Ok(tokio::spawn(async move {
|
|
8023
|
+
loop {
|
|
8024
|
+
let Some(connecting) = endpoint.accept().await else {
|
|
8025
|
+
break;
|
|
8026
|
+
};
|
|
8027
|
+
let state = shared.clone();
|
|
8028
|
+
tokio::spawn(async move {
|
|
8029
|
+
let response = async {
|
|
8030
|
+
let connection = connecting.await.context("accept iroh connection")?;
|
|
8031
|
+
let (mut send, mut recv) =
|
|
8032
|
+
connection.accept_bi().await.context("accept iroh request stream")?;
|
|
8033
|
+
let request_len = recv
|
|
8034
|
+
.read_u32()
|
|
8035
|
+
.await
|
|
8036
|
+
.context("read iroh request length")? as usize;
|
|
8037
|
+
if request_len > P2P_IROH_MAX_MESSAGE_BYTES {
|
|
8038
|
+
return Err(anyhow!(
|
|
8039
|
+
"iroh request too large: {} bytes exceeds limit {}",
|
|
8040
|
+
request_len,
|
|
8041
|
+
P2P_IROH_MAX_MESSAGE_BYTES
|
|
8042
|
+
));
|
|
8043
|
+
}
|
|
8044
|
+
let mut request_bytes = vec![0; request_len];
|
|
8045
|
+
recv.read_exact(&mut request_bytes)
|
|
8046
|
+
.await
|
|
8047
|
+
.context("read iroh request")?;
|
|
8048
|
+
let payload: P2PDirectTcpEnvelope = serde_json::from_slice(&request_bytes)
|
|
8049
|
+
.context("decode iroh request")?;
|
|
8050
|
+
let result = execute_p2p_direct_request(
|
|
8051
|
+
&state,
|
|
8052
|
+
&payload.session_id,
|
|
8053
|
+
&payload.auth_token,
|
|
8054
|
+
&payload.tool,
|
|
8055
|
+
&payload.input,
|
|
8056
|
+
)
|
|
8057
|
+
.await
|
|
8058
|
+
.unwrap_or_else(|err| serde_json::json!({ "ok": false, "error": err.to_string() }));
|
|
8059
|
+
let encoded = serde_json::to_vec(&result).context("encode iroh response")?;
|
|
8060
|
+
send.write_u32(encoded.len() as u32)
|
|
8061
|
+
.await
|
|
8062
|
+
.context("write iroh response length")?;
|
|
8063
|
+
send.write_all(&encoded).await.context("write iroh response")?;
|
|
8064
|
+
send.flush().await.context("flush iroh response")?;
|
|
8065
|
+
send.finish().context("finish iroh response stream")?;
|
|
8066
|
+
connection.closed().await;
|
|
8067
|
+
eprintln!("P2P iroh response sent ({} bytes)", encoded.len());
|
|
8068
|
+
Ok::<(), anyhow::Error>(())
|
|
8069
|
+
}
|
|
8070
|
+
.await;
|
|
8071
|
+
if let Err(err) = response {
|
|
8072
|
+
eprintln!("P2P iroh connection failed: {:#}", err);
|
|
8073
|
+
}
|
|
8074
|
+
});
|
|
8075
|
+
}
|
|
8076
|
+
}))
|
|
8077
|
+
}
|
|
8078
|
+
|
|
8079
|
+
async fn spawn_p2p_direct_server(
|
|
8080
|
+
listen_addr: &str,
|
|
8081
|
+
tcp_listen_addr: &str,
|
|
8082
|
+
iroh_endpoint: Endpoint,
|
|
8083
|
+
allowed_roots: Arc<Vec<std::path::PathBuf>>,
|
|
8084
|
+
client_id: Arc<RwLock<Option<String>>>,
|
|
8085
|
+
project_id: String,
|
|
8086
|
+
base_url: String,
|
|
8087
|
+
access_token: String,
|
|
8088
|
+
) -> Result<Vec<tokio::task::JoinHandle<()>>> {
|
|
8089
|
+
let listener = tokio::net::TcpListener::bind(listen_addr)
|
|
8090
|
+
.await
|
|
8091
|
+
.with_context(|| format!("bind p2p listen address {}", listen_addr))?;
|
|
8092
|
+
let state = P2PDirectServerState {
|
|
8093
|
+
allowed_roots,
|
|
8094
|
+
client_id,
|
|
8095
|
+
project_id,
|
|
8096
|
+
base_url,
|
|
8097
|
+
access_token,
|
|
8098
|
+
};
|
|
8099
|
+
let app = Router::new()
|
|
8100
|
+
.route("/health", get(p2p_direct_health))
|
|
8101
|
+
.route("/execute", post(p2p_direct_execute))
|
|
8102
|
+
.with_state(state.clone());
|
|
8103
|
+
let local_addr: SocketAddr = listener.local_addr().context("resolve p2p server local addr")?;
|
|
8104
|
+
eprintln!("P2P direct server listening on http://{}", local_addr);
|
|
8105
|
+
let server = tokio::spawn(async move {
|
|
8106
|
+
if let Err(err) = axum::serve(listener, app).await {
|
|
8107
|
+
eprintln!("P2P direct server stopped: {}", err);
|
|
8108
|
+
}
|
|
8109
|
+
});
|
|
8110
|
+
let tcp_server = spawn_p2p_tcp_server(tcp_listen_addr, state.clone()).await?;
|
|
8111
|
+
let iroh_server = spawn_p2p_iroh_server(iroh_endpoint, state).await?;
|
|
8112
|
+
Ok(vec![server, tcp_server, iroh_server])
|
|
8113
|
+
}
|
|
8114
|
+
|
|
8115
|
+
pub(crate) async fn link_p2p_execute_command(
|
|
8116
|
+
client: &reqwest::Client,
|
|
8117
|
+
args: LinkP2PExecuteArgs,
|
|
8118
|
+
) -> Result<()> {
|
|
8119
|
+
let auth = resolve_agent_auth(&AgentAuthArgs {
|
|
8120
|
+
base_url: args.base_url.clone(),
|
|
8121
|
+
access_token: args.access_token.clone(),
|
|
8122
|
+
})?;
|
|
8123
|
+
let (base_url, access_token) = match &auth {
|
|
8124
|
+
AgentAuth::Session(session) => (session.base_url.clone(), session.access_token.clone()),
|
|
8125
|
+
AgentAuth::Token {
|
|
8126
|
+
base_url,
|
|
8127
|
+
access_token,
|
|
8128
|
+
} => (base_url.clone(), access_token.clone()),
|
|
8129
|
+
};
|
|
8130
|
+
let mut session: Option<P2PSessionRecord> = None;
|
|
8131
|
+
let mut last_session_error: Option<anyhow::Error> = None;
|
|
8132
|
+
for attempt in 1..=P2P_EXECUTE_SESSION_FETCH_ATTEMPTS {
|
|
8133
|
+
match fetch_p2p_session(
|
|
8134
|
+
client,
|
|
8135
|
+
&base_url,
|
|
8136
|
+
&access_token,
|
|
8137
|
+
&args.project_id,
|
|
8138
|
+
&args.session_id,
|
|
8139
|
+
)
|
|
8140
|
+
.await
|
|
8141
|
+
{
|
|
8142
|
+
Ok(value) => {
|
|
8143
|
+
session = Some(value);
|
|
8144
|
+
last_session_error = None;
|
|
8145
|
+
break;
|
|
8146
|
+
}
|
|
8147
|
+
Err(err) => {
|
|
8148
|
+
last_session_error = Some(err.context(format!(
|
|
8149
|
+
"fetch p2p session attempt {} of {}",
|
|
8150
|
+
attempt, P2P_EXECUTE_SESSION_FETCH_ATTEMPTS
|
|
8151
|
+
)));
|
|
8152
|
+
if attempt < P2P_EXECUTE_SESSION_FETCH_ATTEMPTS {
|
|
8153
|
+
sleep(Duration::from_millis(P2P_EXECUTE_RETRY_DELAY_MS * attempt as u64)).await;
|
|
8154
|
+
}
|
|
8155
|
+
}
|
|
8156
|
+
}
|
|
8157
|
+
}
|
|
8158
|
+
let session = session.ok_or_else(|| {
|
|
8159
|
+
last_session_error.unwrap_or_else(|| anyhow!("failed to fetch p2p session"))
|
|
8160
|
+
})?;
|
|
8161
|
+
if session.status != "ready" {
|
|
8162
|
+
return Err(anyhow!("p2p session {} is not ready", session.session_id));
|
|
8163
|
+
}
|
|
8164
|
+
let (_signal, peer, auth_token) =
|
|
8165
|
+
resolve_target_signal(&session, args.target_role.as_api_str())?;
|
|
8166
|
+
let input = load_optional_json_body(args.input.as_deref())?
|
|
8167
|
+
.unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
|
|
8168
|
+
let input = serde_json::Value::Object(parse_object_from_value(input, "p2p execute input")?);
|
|
8169
|
+
let mut last_error: Option<anyhow::Error> = None;
|
|
8170
|
+
let mut response_payload = serde_json::json!({});
|
|
8171
|
+
let mut status = StatusCode::BAD_GATEWAY;
|
|
8172
|
+
let mut resolved_endpoint = peer.endpoint.clone();
|
|
8173
|
+
if let Some(iroh_addr_json) = peer.iroh_addr_json.as_deref() {
|
|
8174
|
+
for attempt in 1..=P2P_EXECUTE_ENDPOINT_ATTEMPTS {
|
|
8175
|
+
match invoke_p2p_iroh_endpoint(
|
|
8176
|
+
iroh_addr_json,
|
|
8177
|
+
&auth_token,
|
|
8178
|
+
&args.session_id,
|
|
8179
|
+
&args.tool,
|
|
8180
|
+
&input,
|
|
8181
|
+
)
|
|
8182
|
+
.await
|
|
8183
|
+
{
|
|
8184
|
+
Ok((response_status, payload)) => {
|
|
8185
|
+
status = response_status;
|
|
8186
|
+
response_payload = payload;
|
|
8187
|
+
resolved_endpoint = "iroh://direct".to_string();
|
|
8188
|
+
if status.is_success() || !p2p_should_retry_status(status) || attempt == P2P_EXECUTE_ENDPOINT_ATTEMPTS {
|
|
8189
|
+
last_error = None;
|
|
8190
|
+
break;
|
|
8191
|
+
}
|
|
8192
|
+
}
|
|
8193
|
+
Err(err) => {
|
|
8194
|
+
eprintln!("P2P iroh direct attempt failed: {:#}", err);
|
|
8195
|
+
last_error = Some(anyhow!(err).context(format!(
|
|
8196
|
+
"call p2p iroh endpoint attempt {} of {}",
|
|
8197
|
+
attempt, P2P_EXECUTE_ENDPOINT_ATTEMPTS
|
|
8198
|
+
)));
|
|
8199
|
+
if attempt < P2P_EXECUTE_ENDPOINT_ATTEMPTS {
|
|
8200
|
+
sleep(Duration::from_millis(P2P_EXECUTE_RETRY_DELAY_MS * attempt as u64)).await;
|
|
8201
|
+
continue;
|
|
8202
|
+
}
|
|
8203
|
+
}
|
|
8204
|
+
}
|
|
8205
|
+
}
|
|
8206
|
+
}
|
|
8207
|
+
if status.is_success() {
|
|
8208
|
+
let output = serde_json::json!({
|
|
8209
|
+
"ok": true,
|
|
8210
|
+
"peer": {
|
|
8211
|
+
"endpoint": resolved_endpoint,
|
|
8212
|
+
"role": peer.role,
|
|
8213
|
+
"fromClientId": peer.from_client_id,
|
|
8214
|
+
},
|
|
8215
|
+
"status": status.as_u16(),
|
|
8216
|
+
"body": response_payload,
|
|
8217
|
+
});
|
|
8218
|
+
print_json(&output)?;
|
|
8219
|
+
return Ok(());
|
|
8220
|
+
}
|
|
8221
|
+
for endpoint in candidate_peer_endpoints(&peer.endpoint) {
|
|
8222
|
+
for attempt in 1..=P2P_EXECUTE_ENDPOINT_ATTEMPTS {
|
|
8223
|
+
let result = if endpoint.starts_with("tcp://") {
|
|
8224
|
+
invoke_p2p_tcp_endpoint(
|
|
8225
|
+
&endpoint,
|
|
8226
|
+
&auth_token,
|
|
8227
|
+
&args.session_id,
|
|
8228
|
+
&args.tool,
|
|
8229
|
+
&input,
|
|
8230
|
+
)
|
|
8231
|
+
.await
|
|
8232
|
+
} else {
|
|
8233
|
+
invoke_p2p_http_endpoint(
|
|
8234
|
+
client,
|
|
8235
|
+
&endpoint,
|
|
8236
|
+
&auth_token,
|
|
8237
|
+
&args.session_id,
|
|
8238
|
+
&args.tool,
|
|
8239
|
+
&input,
|
|
8240
|
+
)
|
|
8241
|
+
.await
|
|
8242
|
+
};
|
|
8243
|
+
match result {
|
|
8244
|
+
Ok((response_status, payload)) => {
|
|
8245
|
+
status = response_status;
|
|
8246
|
+
response_payload = payload;
|
|
8247
|
+
resolved_endpoint = endpoint.clone();
|
|
8248
|
+
if status.is_success() || !p2p_should_retry_status(status) || attempt == P2P_EXECUTE_ENDPOINT_ATTEMPTS {
|
|
8249
|
+
last_error = None;
|
|
8250
|
+
break;
|
|
8251
|
+
}
|
|
8252
|
+
sleep(Duration::from_millis(P2P_EXECUTE_RETRY_DELAY_MS * attempt as u64)).await;
|
|
8253
|
+
}
|
|
8254
|
+
Err(err) => {
|
|
8255
|
+
last_error = Some(anyhow!(err).context(format!(
|
|
8256
|
+
"call p2p direct endpoint {} attempt {} of {}",
|
|
8257
|
+
endpoint, attempt, P2P_EXECUTE_ENDPOINT_ATTEMPTS
|
|
8258
|
+
)));
|
|
8259
|
+
if attempt < P2P_EXECUTE_ENDPOINT_ATTEMPTS {
|
|
8260
|
+
sleep(Duration::from_millis(P2P_EXECUTE_RETRY_DELAY_MS * attempt as u64)).await;
|
|
8261
|
+
continue;
|
|
8262
|
+
}
|
|
8263
|
+
}
|
|
8264
|
+
}
|
|
8265
|
+
if status.is_success() || !p2p_should_retry_status(status) {
|
|
8266
|
+
break;
|
|
8267
|
+
}
|
|
8268
|
+
}
|
|
8269
|
+
if status.is_success() || !p2p_should_retry_status(status) {
|
|
8270
|
+
break;
|
|
8271
|
+
}
|
|
8272
|
+
}
|
|
8273
|
+
if let Some(err) = last_error {
|
|
8274
|
+
return Err(err);
|
|
8275
|
+
}
|
|
8276
|
+
let output = serde_json::json!({
|
|
8277
|
+
"ok": status.is_success() && response_payload.get("ok").and_then(|value| value.as_bool()).unwrap_or(false),
|
|
8278
|
+
"peer": {
|
|
8279
|
+
"endpoint": resolved_endpoint,
|
|
8280
|
+
"role": peer.role,
|
|
8281
|
+
"fromClientId": peer.from_client_id,
|
|
8282
|
+
},
|
|
8283
|
+
"status": status.as_u16(),
|
|
8284
|
+
"body": response_payload,
|
|
8285
|
+
});
|
|
8286
|
+
print_json(&output)?;
|
|
8287
|
+
if !status.is_success() {
|
|
8288
|
+
return Err(anyhow!("link p2p execute failed with status {}", status));
|
|
8289
|
+
}
|
|
8290
|
+
Ok(())
|
|
8291
|
+
}
|
|
8292
|
+
|
|
7542
8293
|
async fn handle_ws_message(
|
|
7543
8294
|
text: &str,
|
|
7544
8295
|
allowed_roots: &[std::path::PathBuf],
|
|
8296
|
+
registered_client_id: &Arc<RwLock<Option<String>>>,
|
|
7545
8297
|
ws_stream: &mut (impl futures_util::Sink<
|
|
7546
8298
|
tokio_tungstenite::tungstenite::Message,
|
|
7547
8299
|
Error = tokio_tungstenite::tungstenite::Error,
|
|
@@ -7562,6 +8314,7 @@ async fn handle_ws_message(
|
|
|
7562
8314
|
match msg_type {
|
|
7563
8315
|
"registered" => {
|
|
7564
8316
|
let client_id = msg.get("clientId").and_then(|v| v.as_str()).unwrap_or("?");
|
|
8317
|
+
*registered_client_id.write().await = Some(client_id.to_string());
|
|
7565
8318
|
eprintln!("Registered as {}", client_id);
|
|
7566
8319
|
}
|
|
7567
8320
|
"tool_request" => {
|
|
@@ -7635,14 +8388,22 @@ fn resolve_safe_path_allow_missing(
|
|
|
7635
8388
|
) -> Option<std::path::PathBuf> {
|
|
7636
8389
|
for root in allowed_roots {
|
|
7637
8390
|
let candidate = root.join(path_str);
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
8391
|
+
let canonical_root = match root.canonicalize() {
|
|
8392
|
+
Ok(value) => value,
|
|
8393
|
+
Err(_) => continue,
|
|
8394
|
+
};
|
|
8395
|
+
if !candidate.starts_with(root) {
|
|
8396
|
+
continue;
|
|
8397
|
+
}
|
|
8398
|
+
let mut probe = Some(candidate.as_path());
|
|
8399
|
+
while let Some(current) = probe {
|
|
8400
|
+
if let Ok(resolved_current) = current.canonicalize() {
|
|
8401
|
+
if resolved_current.starts_with(&canonical_root) {
|
|
7641
8402
|
return Some(candidate);
|
|
7642
8403
|
}
|
|
8404
|
+
break;
|
|
7643
8405
|
}
|
|
7644
|
-
|
|
7645
|
-
return Some(candidate);
|
|
8406
|
+
probe = current.parent();
|
|
7646
8407
|
}
|
|
7647
8408
|
}
|
|
7648
8409
|
None
|
|
@@ -7686,6 +8447,7 @@ async fn execute_read_tool(
|
|
|
7686
8447
|
return execute_read_range_tool(input, allowed_roots).await;
|
|
7687
8448
|
}
|
|
7688
8449
|
let path_str = input.get("path").and_then(|v| v.as_str()).unwrap_or("");
|
|
8450
|
+
let encoding = input.get("encoding").and_then(|v| v.as_str()).unwrap_or("utf8");
|
|
7689
8451
|
if path_str.is_empty() {
|
|
7690
8452
|
return serde_json::json!({"ok": false, "error": "path required"});
|
|
7691
8453
|
}
|
|
@@ -7693,8 +8455,17 @@ async fn execute_read_tool(
|
|
|
7693
8455
|
Some(p) => p,
|
|
7694
8456
|
None => return serde_json::json!({"ok": false, "error": "path not within allowed folders"}),
|
|
7695
8457
|
};
|
|
7696
|
-
match tokio::fs::
|
|
7697
|
-
Ok(content) =>
|
|
8458
|
+
match tokio::fs::read(&resolved).await {
|
|
8459
|
+
Ok(content) => {
|
|
8460
|
+
if encoding == "base64" {
|
|
8461
|
+
serde_json::json!({"ok": true, "data": BASE64_STANDARD.encode(content)})
|
|
8462
|
+
} else {
|
|
8463
|
+
match String::from_utf8(content) {
|
|
8464
|
+
Ok(text) => serde_json::json!({"ok": true, "data": text}),
|
|
8465
|
+
Err(error) => serde_json::json!({"ok": false, "error": format!("file is not valid utf-8: {}", error)}),
|
|
8466
|
+
}
|
|
8467
|
+
}
|
|
8468
|
+
}
|
|
7698
8469
|
Err(e) => serde_json::json!({"ok": false, "error": e.to_string()}),
|
|
7699
8470
|
}
|
|
7700
8471
|
}
|
|
@@ -8003,7 +8774,22 @@ async fn execute_write_tool(
|
|
|
8003
8774
|
allowed_roots: &[std::path::PathBuf],
|
|
8004
8775
|
) -> serde_json::Value {
|
|
8005
8776
|
let path_str = input.get("path").and_then(|v| v.as_str()).unwrap_or("");
|
|
8006
|
-
let
|
|
8777
|
+
let encoding = input.get("encoding").and_then(|v| v.as_str()).unwrap_or_else(|| {
|
|
8778
|
+
if input.get("contentBase64").and_then(|v| v.as_str()).is_some() {
|
|
8779
|
+
"base64"
|
|
8780
|
+
} else {
|
|
8781
|
+
"utf8"
|
|
8782
|
+
}
|
|
8783
|
+
});
|
|
8784
|
+
let content = if encoding == "base64" {
|
|
8785
|
+
input
|
|
8786
|
+
.get("contentBase64")
|
|
8787
|
+
.and_then(|v| v.as_str())
|
|
8788
|
+
.or_else(|| input.get("content").and_then(|v| v.as_str()))
|
|
8789
|
+
.unwrap_or("")
|
|
8790
|
+
} else {
|
|
8791
|
+
input.get("content").and_then(|v| v.as_str()).unwrap_or("")
|
|
8792
|
+
};
|
|
8007
8793
|
let create_parents = input
|
|
8008
8794
|
.get("createParents")
|
|
8009
8795
|
.and_then(|v| v.as_bool())
|
|
@@ -8034,7 +8820,16 @@ async fn execute_write_tool(
|
|
|
8034
8820
|
return serde_json::json!({"ok": false, "error": format!("file already exists: {}", path_str)});
|
|
8035
8821
|
}
|
|
8036
8822
|
|
|
8037
|
-
|
|
8823
|
+
let payload = if encoding == "base64" {
|
|
8824
|
+
match BASE64_STANDARD.decode(content) {
|
|
8825
|
+
Ok(bytes) => bytes,
|
|
8826
|
+
Err(error) => return serde_json::json!({"ok": false, "error": format!("invalid base64 content: {}", error)}),
|
|
8827
|
+
}
|
|
8828
|
+
} else {
|
|
8829
|
+
content.as_bytes().to_vec()
|
|
8830
|
+
};
|
|
8831
|
+
|
|
8832
|
+
match tokio::fs::write(&resolved, payload).await {
|
|
8038
8833
|
Ok(_) => match tokio::fs::metadata(&resolved).await {
|
|
8039
8834
|
Ok(metadata) => serde_json::json!({
|
|
8040
8835
|
"ok": true,
|