From 461ef8030de8dfd3dd0f6f617d293b972ce4cfe4 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Thu, 15 Oct 2026 08:49:56 +0200 Subject: [PATCH] =?UTF-8?q?Lot=206=20:=20clients=20Rust=20du=20chiffrement?= =?UTF-8?q?=20et=20de=20l'ex=C3=A9cution=20des=20outils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sceller / ouvrir un blob par le Go (mémoire verrouillée : erreur dédiée, rien d'écrit en clair) et faire exécuter un appel d'outil par sa boucle — les deux ponts dont la conversation Rust aura besoin. Co-Authored-By: Claude Opus 5.5 --- rust/loki-rs/src/store.rs | 48 +++++++++++++++++++++++++++++ rust/loki-rs/tests/common/fakego.rs | 28 +++++++++++++++++ rust/loki-rs/tests/lot6.rs | 45 +++++++++++++++++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 rust/loki-rs/tests/lot6.rs diff --git a/rust/loki-rs/src/store.rs b/rust/loki-rs/src/store.rs index 43749e4..7cdd6f5 100644 --- a/rust/loki-rs/src/store.rs +++ b/rust/loki-rs/src/store.rs @@ -86,6 +86,46 @@ impl Store { serde_json::from_slice(&bytes).map_err(|e| e.to_string()) } + /// Scelle un blob avec la clé de la mémoire (gardée par le Go). Clair + /// rendu tel quel si le chiffrement est coupé. + pub async fn seal(&self, plain: Vec) -> Result, CryptError> { + self.crypt("/internal/v1/crypt/seal", plain).await + } + + /// Ouvre un blob (un contenu non chiffré passe tel quel). + pub async fn open(&self, raw: Vec) -> Result, CryptError> { + self.crypt("/internal/v1/crypt/open", raw).await + } + + async fn crypt(&self, path: &str, body: Vec) -> Result, CryptError> { + match self.call(Method::POST, path, body).await { + Ok((StatusCode::OK, b)) => Ok(b.to_vec()), + Ok((StatusCode::LOCKED, _)) => Err(CryptError::Locked), + Ok((s, b)) => Err(CryptError::Other(format!( + "{path} : {s} {}", + String::from_utf8_lossy(&b) + ))), + Err(e) => Err(CryptError::Other(e)), + } + } + + /// Exécute un appel d'outil par le Go (`/internal/v1/tool/exec`) : même + /// fonction que sa boucle de chat. `caps` : objet JSON des capacités du + /// tour (champs Go : Agent, Internet, Mem, Code, Role, ComputerUse). + pub async fn tool_exec( + &self, + name: &str, + args: &serde_json::Value, + caps: &serde_json::Value, + ) -> Result { + self.internal_json( + Method::POST, + "/internal/v1/tool/exec", + Some(&serde_json::json!({ "name": name, "args": args, "caps": caps })), + ) + .await + } + /// Valeur brute ; `Ok(None)` = absente, `Err` = pas pu lire (à distinguer /// pour les secrets : comme `getBytesErr`). pub async fn get(&self, bucket: &str, key: &str) -> Result>, String> { @@ -136,6 +176,14 @@ impl Store { } } +/// Échec de chiffrement / déchiffrement. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CryptError { + /// Mémoire verrouillée : attendre le déverrouillage, ne rien écrire. + Locked, + Other(String), +} + /// Encodage d'un paramètre de requête (RFC 3986, caractères non réservés /// conservés). pub fn enc(s: &str) -> String { diff --git a/rust/loki-rs/tests/common/fakego.rs b/rust/loki-rs/tests/common/fakego.rs index 2923e88..b9c3b87 100644 --- a/rust/loki-rs/tests/common/fakego.rs +++ b/rust/loki-rs/tests/common/fakego.rs @@ -30,6 +30,10 @@ pub struct FakeGo { pub side_live: Mutex, pub refuse: Mutex>, pub oai_log: Mutex>, + /// Mémoire verrouillée : seal/open répondent 423. + pub locked: Mutex, + /// Adresse d'écoute du faux Go (pour un client `Store` direct). + pub backend: Mutex>, } impl FakeGo { @@ -101,6 +105,29 @@ async fn handle(st: Arc, req: Request) -> Response let key = st.get("state", "web_key").unwrap_or_default(); resp(200, format!(r#"{{"web_key":"{key}","version":"0.15.0"}}"#)) } + "/internal/v1/crypt/seal" | "/internal/v1/crypt/open" => { + if *st.locked.lock().unwrap() { + return resp(423, r#"{"error":"mémoire verrouillée"}"#); + } + let body = req.into_body().collect().await.unwrap().to_bytes(); + if path.ends_with("seal") { + let mut out = b"SCELLE:".to_vec(); + out.extend_from_slice(&body); + resp(200, out) + } else { + resp(200, body.strip_prefix(b"SCELLE:").unwrap_or(&body).to_vec()) + } + } + "/internal/v1/tool/exec" => { + let body = req.into_body().collect().await.unwrap().to_bytes(); + let v: serde_json::Value = serde_json::from_slice(&body).unwrap(); + let out = serde_json::json!({ + "label": v["args"]["command"], + "result": format!("exec:{}", v["name"].as_str().unwrap()), + "caps_agent": v["caps"]["Agent"], + }); + resp(200, out.to_string()) + } "/internal/v1/oai/route" => { let key = st.get("state", "api_key").unwrap_or_default(); let v = serde_json::json!({ @@ -188,6 +215,7 @@ pub async fn start(home: std::path::PathBuf) -> (SocketAddr, Arc, onesho let st = Arc::new(FakeGo::default()); let l = TcpListener::bind("127.0.0.1:0").await.unwrap(); let backend = l.local_addr().unwrap(); + *st.backend.lock().unwrap() = Some(backend); let st2 = st.clone(); tokio::spawn(async move { while let Ok((s, _)) = l.accept().await { diff --git a/rust/loki-rs/tests/lot6.rs b/rust/loki-rs/tests/lot6.rs new file mode 100644 index 0000000..6bf0bb1 --- /dev/null +++ b/rust/loki-rs/tests/lot6.rs @@ -0,0 +1,45 @@ +//! Lot 6 : clients Rust des API internes du chat (chiffrement, outils). + +mod common; + +use common::fakego; +use loki_rs::store::{CryptError, Store}; + +async fn store() -> ( + Store, + std::sync::Arc, + tokio::sync::oneshot::Sender<()>, + tempfile::TempDir, +) { + let t = tempfile::tempdir().unwrap(); + let (_f, go, stop) = fakego::start(t.path().into()).await; + let backend = go.backend.lock().unwrap().unwrap(); + (Store::new(backend, fakego::JETON.into()), go, stop, t) +} + +#[tokio::test] +async fn sceller_ouvrir_et_verrou() { + let (st, go, _stop, _t) = store().await; + let sealed = st.seal(b"bonjour".to_vec()).await.unwrap(); + assert_eq!(sealed, b"SCELLE:bonjour"); + assert_eq!(st.open(sealed).await.unwrap(), b"bonjour"); + *go.locked.lock().unwrap() = true; + assert_eq!(st.seal(b"x".to_vec()).await, Err(CryptError::Locked)); + assert_eq!(st.open(b"x".to_vec()).await, Err(CryptError::Locked)); +} + +#[tokio::test] +async fn outil_execute_par_le_go() { + let (st, _go, _stop, _t) = store().await; + let v = st + .tool_exec( + "bash", + &serde_json::json!({ "command": "ls" }), + &serde_json::json!({ "Agent": true }), + ) + .await + .unwrap(); + assert_eq!(v["result"], "exec:bash"); + assert_eq!(v["label"], "ls"); + assert_eq!(v["caps_agent"], true); +}