From 3299468df6bfdbae9f7aa4a9569fdbfe8fb9de99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=AA=E7=9F=A5=E6=97=B6=E5=85=89?= <732857315@qq.com> Date: Wed, 10 Jun 2026 14:27:46 +0800 Subject: [PATCH] Migrate test plugins to Rust and async-ify syscall modules - Replace Python test plugins (echo, ai_test, claudecode) with Rust implementations driven by the plugin SDK - Convert fs/network/process/timer syscall modules to async/await - Change Network.OpenConn to return stream ConnEvent (conn_id handed to client in first "open" frame for Send/Close routing) - Bind timer callbacks to plugin identity; stream_call subscriptions are identity-checked - Update docs and regenerate Python SDK protobuf stubs Verified: cargo build/test/clippy clean; echo + ai_test full syscall suites pass; hermes & claudecode register and heartbeat; cross-plugin bridge.call routing and auth denial verified end-to-end. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 143 +++++++++++ README.md | 4 +- core/agentsd/Cargo.toml | 2 + core/agentsd/src/callback.rs | 29 ++- core/agentsd/src/permission.rs | 8 +- core/agentsd/src/plugin.rs | 14 +- core/agentsd/src/server.rs | 234 ++++++++++++++---- core/agentsd/src/syscall/fs.rs | 57 +++++ core/agentsd/src/syscall/network.rs | 196 ++++++++++++++- core/agentsd/src/syscall/process.rs | 117 +++++---- core/agentsd/src/syscall/timer.rs | 50 ++++ docs/01-内核系统调用.md | 19 +- docs/02-插件模型.md | 22 ++ docs/03-实施路线.md | 21 +- docs/04-项目结构.md | 19 +- plugins/_sdk/agentsd_pb2.py | 72 +++--- plugins/_sdk/agentsd_pb2_grpc.py | 6 +- plugins/ai_test/ai_test_plugin.py | 222 ----------------- plugins/ai_test/plugin.yaml | 2 +- plugins/claudecode/claudecode_plugin.py | 312 ------------------------ plugins/claudecode/plugin.yaml | 2 +- plugins/claudecode/test_chat.py | 29 --- plugins/echo/echo_plugin.py | 67 ----- plugins/echo/plugin.yaml | 2 +- plugins/hermes/hermes_plugin.py | 6 +- proto/agentsd.proto | 2 +- 26 files changed, 843 insertions(+), 814 deletions(-) delete mode 100644 plugins/ai_test/ai_test_plugin.py delete mode 100644 plugins/claudecode/claudecode_plugin.py delete mode 100644 plugins/claudecode/test_chat.py delete mode 100644 plugins/echo/echo_plugin.py diff --git a/Cargo.lock b/Cargo.lock index 31433c9..eccd826 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,8 @@ version = "0.1.0" dependencies = [ "agentsd-proto", "anyhow", + "chrono", + "cron", "libc", "notify", "prost", @@ -123,6 +125,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -273,6 +284,36 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cron" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8c3e73077b4b4a6ab1ea5047c37c57aee77657bc8ecd6f29b0af082d0b0c07" +dependencies = [ + "chrono", + "nom", + "once_cell", +] + [[package]] name = "displaydoc" version = "0.2.6" @@ -638,6 +679,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -939,6 +1004,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "mio" version = "1.2.1" @@ -957,6 +1028,16 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "notify" version = "7.0.0" @@ -994,6 +1075,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -2181,12 +2271,65 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/README.md b/README.md index 2c60cfb..dac6843 100644 --- a/README.md +++ b/README.md @@ -71,14 +71,14 @@ agentsd ← 对标 Linux 内核 + systemd(PID 1) ## 当前状态 -阶段 1 已完成:内核骨架可运行,9 个 syscall gRPC service + PluginBridge 已注册,Python 测试插件验证全链路通过。默认运行数据写入程序所在目录的 `data/` 文件夹。`plugins/ai_test` 提供手动 AI 验证插件,默认不开启/不自动加载。Release 二进制约 6.1MB。 +阶段 1 已完成:内核骨架可运行,9 个 syscall gRPC service + PluginBridge 已注册,测试插件验证全链路通过。默认运行数据写入程序所在目录的 `data/` 文件夹。`plugins/ai_test` 提供手动 AI 验证插件,默认不开启/不自动加载。Release 二进制约 6.1MB。 ## 手动验证 ```bash cargo build --release RUST_LOG=agentsd=info ./target/release/agentsd.exe -python plugins/ai_test/ai_test_plugin.py +cargo run -p agentsd-plugin-ai-test ``` `plugins/ai_test/plugin.yaml` 中设置了 `enabled: false` 和 `autoload: false`,未来实现插件自动加载时应默认跳过它。 diff --git a/core/agentsd/Cargo.toml b/core/agentsd/Cargo.toml index dfbcd42..400b00f 100644 --- a/core/agentsd/Cargo.toml +++ b/core/agentsd/Cargo.toml @@ -23,3 +23,5 @@ uuid = { workspace = true } notify = { workspace = true } reqwest = { workspace = true } libc = { workspace = true } +cron = "0.12" +chrono = "0.4" diff --git a/core/agentsd/src/callback.rs b/core/agentsd/src/callback.rs index b0b33be..e42c23d 100644 --- a/core/agentsd/src/callback.rs +++ b/core/agentsd/src/callback.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::sync::Mutex; +use std::sync::atomic::{AtomicU64, Ordering}; use tokio::sync::mpsc; /// A fired timer event delivered to the plugin that created it. @@ -11,7 +12,8 @@ pub struct TimerEvent { /// Manages per-plugin event channels for timer callbacks. pub struct CallbackBus { - senders: Mutex>>, + senders: Mutex, u64)>>, + generation: AtomicU64, } impl Default for CallbackBus { @@ -24,23 +26,27 @@ impl CallbackBus { pub fn new() -> Self { Self { senders: Mutex::new(HashMap::new()), + generation: AtomicU64::new(0), } } /// Register a plugin and get a receiver for its timer events. - pub fn subscribe(&self, plugin_id: &str) -> mpsc::Receiver { + /// Returns the receiver and a generation counter to be used with + /// [`remove_if_generation`] for safe cleanup. + pub fn subscribe(&self, plugin_id: &str) -> (mpsc::Receiver, u64) { let (tx, rx) = mpsc::channel(64); + let gen = self.generation.fetch_add(1, Ordering::Relaxed); self.senders .lock() .unwrap() - .insert(plugin_id.to_string(), tx); - rx + .insert(plugin_id.to_string(), (tx, gen)); + (rx, gen) } /// Deliver a timer event to the target plugin. pub fn fire(&self, plugin_id: &str, event: TimerEvent) { let senders = self.senders.lock().unwrap(); - if let Some(tx) = senders.get(plugin_id) { + if let Some((tx, _gen)) = senders.get(plugin_id) { match tx.try_send(event) { Ok(()) => {} Err(mpsc::error::TrySendError::Full(_)) => { @@ -57,7 +63,20 @@ impl CallbackBus { } /// Remove a plugin's channel (on unregister/disconnect). + #[allow(dead_code)] pub fn remove(&self, plugin_id: &str) { self.senders.lock().unwrap().remove(plugin_id); } + + /// Remove a plugin's channel only if the generation counter matches. + /// This prevents a stale unsubscribe from accidentally removing a newer + /// subscription that replaced it. + pub fn remove_if_generation(&self, plugin_id: &str, gen: u64) { + let mut senders = self.senders.lock().unwrap(); + if let Some((_, current_gen)) = senders.get(plugin_id) { + if *current_gen == gen { + senders.remove(plugin_id); + } + } + } } diff --git a/core/agentsd/src/permission.rs b/core/agentsd/src/permission.rs index b74b89c..c7b298a 100644 --- a/core/agentsd/src/permission.rs +++ b/core/agentsd/src/permission.rs @@ -46,8 +46,12 @@ pub fn check_permission( syscall_name: &str, ) -> Result { let Some(plugin_id) = plugin_id_from_metadata(req_metadata) else { - // Direct/internal/manual testing call. - return Ok(String::new()); + // The server listens on TCP: an absent plugin id means an unidentified + // local process, not a trusted internal call. Reject instead of allow-all. + return Err(Status::unauthenticated(format!( + "syscall '{}' requires x-plugin-id metadata from a registered plugin", + syscall_name + ))); }; let record = require_running_plugin(plugins, &plugin_id)?; diff --git a/core/agentsd/src/plugin.rs b/core/agentsd/src/plugin.rs index a4304e9..64f01ae 100644 --- a/core/agentsd/src/plugin.rs +++ b/core/agentsd/src/plugin.rs @@ -120,10 +120,18 @@ impl PluginManager { } fn save_to_disk(&self) { - let plugins = self.plugins.lock().unwrap(); - let records: Vec<&PluginRecord> = plugins.values().collect(); + let records: Vec = { + let plugins = self.plugins.lock().unwrap(); + plugins.values().cloned().collect() + }; + // Lock released here — serialize and write outside the lock if let Ok(json) = serde_json::to_string_pretty(&records) { - let _ = std::fs::write(Self::state_path(), json); + // Write to temp file first, then rename for atomicity + let path = Self::state_path(); + let tmp = path.with_extension("json.tmp"); + if std::fs::write(&tmp, &json).is_ok() { + let _ = std::fs::rename(&tmp, &path); + } } } diff --git a/core/agentsd/src/server.rs b/core/agentsd/src/server.rs index 9c33f7c..c5d23bd 100644 --- a/core/agentsd/src/server.rs +++ b/core/agentsd/src/server.rs @@ -9,9 +9,11 @@ use agentsd_proto::agentsd::{ plugin_bridge_server::PluginBridge as PluginBridgeService, process_server::Process as ProcessService, timer_server::Timer as TimerService, }; -use std::sync::Arc; +use std::sync::{Arc, OnceLock}; use tonic::{Request, Response, Status}; +static REGISTER_TOKEN: OnceLock> = OnceLock::new(); + pub struct AgentsdServer { kernel: Arc, plugins: Arc, @@ -231,10 +233,9 @@ impl AudioService for AudioSvc { req: Request, ) -> Result, Status> { auth(&self.plugins, &req, "audio.record_stream")?; - let (_tx, rx) = tokio::sync::mpsc::channel(16); - Ok(Response::new(tokio_stream::wrappers::ReceiverStream::new( - rx, - ))) + Err(Status::unimplemented( + "audio.record_stream not yet implemented (requires audio driver plugin)", + )) } } @@ -366,7 +367,24 @@ impl FsService for FsSvc { req: Request, ) -> Result, Status> { auth(&self.plugins, &req, "fs.watch")?; - let (_tx, rx) = tokio::sync::mpsc::channel(16); + let r = req.into_inner(); + let mut events = self + .kernel + .fs + .watch(&r.path) + .map_err(Status::invalid_argument)?; + let (tx, rx) = tokio::sync::mpsc::channel(64); + tokio::spawn(async move { + while let Some(ev) = events.recv().await { + let event = FsWatchEvent { + path: ev.path, + kind: ev.kind, + }; + if tx.send(Ok(event)).await.is_err() { + break; + } + } + }); Ok(Response::new(tokio_stream::wrappers::ReceiverStream::new( rx, ))) @@ -518,14 +536,49 @@ impl NetworkService for NetworkSvc { } } - type OpenConnStream = tokio_stream::wrappers::ReceiverStream>; + type OpenConnStream = tokio_stream::wrappers::ReceiverStream>; async fn open_conn( &self, req: Request, ) -> Result, Status> { auth(&self.plugins, &req, "network.open_conn")?; - let (_tx, rx) = tokio::sync::mpsc::channel(16); + let r = req.into_inner(); + let (conn_id, mut inbound) = self + .kernel + .network + .open_conn(&r.addr, &r.protocol) + .await + .map_err(Status::unavailable)?; + let (tx, rx) = tokio::sync::mpsc::channel(64); + tokio::spawn(async move { + // First frame hands the conn_id to the client for Send/Close routing. + let open_event = ConnEvent { + conn_id: conn_id.clone(), + data: vec![], + kind: "open".to_string(), + }; + if tx.send(Ok(open_event)).await.is_err() { + return; + } + while let Some(data) = inbound.recv().await { + let event = ConnEvent { + conn_id: conn_id.clone(), + data, + kind: "data".to_string(), + }; + if tx.send(Ok(event)).await.is_err() { + break; + } + } + let _ = tx + .send(Ok(ConnEvent { + conn_id: conn_id.clone(), + data: vec![], + kind: "closed".to_string(), + })) + .await; + }); Ok(Response::new(tokio_stream::wrappers::ReceiverStream::new( rx, ))) @@ -538,7 +591,51 @@ impl NetworkService for NetworkSvc { req: Request, ) -> Result, Status> { auth(&self.plugins, &req, "network.listen")?; - let (_tx, rx) = tokio::sync::mpsc::channel(16); + let r = req.into_inner(); + let mut accepted = self + .kernel + .network + .listen(r.port, &r.protocol) + .await + .map_err(Status::unavailable)?; + let (tx, rx) = tokio::sync::mpsc::channel(64); + let network = self.kernel.network.clone(); + tokio::spawn(async move { + while let Some(conn) = accepted.recv().await { + let accept_event = ConnEvent { + conn_id: conn.conn_id.clone(), + data: conn.peer.clone().into_bytes(), + kind: "accept".to_string(), + }; + if tx.send(Ok(accept_event)).await.is_err() { + break; + } + // Forward this connection's inbound data onto the same event stream. + if let Some(mut inbound) = network.take_inbound(&conn.conn_id).await { + let tx_data = tx.clone(); + let cid = conn.conn_id.clone(); + tokio::spawn(async move { + while let Some(data) = inbound.recv().await { + let event = ConnEvent { + conn_id: cid.clone(), + data, + kind: "data".to_string(), + }; + if tx_data.send(Ok(event)).await.is_err() { + break; + } + } + let _ = tx_data + .send(Ok(ConnEvent { + conn_id: cid.clone(), + data: vec![], + kind: "closed".to_string(), + })) + .await; + }); + } + } + }); Ok(Response::new(tokio_stream::wrappers::ReceiverStream::new( rx, ))) @@ -546,18 +643,26 @@ impl NetworkService for NetworkSvc { async fn send(&self, req: Request) -> Result, Status> { auth(&self.plugins, &req, "network.send")?; - Ok(Response::new(NetResponse { - ok: false, - error: "send: not yet implemented".to_string(), - })) + let r = req.into_inner(); + match self.kernel.network.send(&r.conn_id, &r.data).await { + Ok(()) => Ok(Response::new(NetResponse { + ok: true, + error: String::new(), + })), + Err(e) => Ok(Response::new(NetResponse { ok: false, error: e })), + } } async fn close(&self, req: Request) -> Result, Status> { auth(&self.plugins, &req, "network.close")?; - Ok(Response::new(NetResponse { - ok: false, - error: "close: not yet implemented".to_string(), - })) + let r = req.into_inner(); + match self.kernel.network.close(&r.conn_id).await { + Ok(()) => Ok(Response::new(NetResponse { + ok: true, + error: String::new(), + })), + Err(e) => Ok(Response::new(NetResponse { ok: false, error: e })), + } } async fn available( @@ -617,10 +722,7 @@ impl InputService for InputSvc { async fn events(&self, req: Request) -> Result, Status> { auth(&self.plugins, &req, "input.events")?; - let (_tx, rx) = tokio::sync::mpsc::channel(16); - Ok(Response::new(tokio_stream::wrappers::ReceiverStream::new( - rx, - ))) + Err(Status::unimplemented("input.events not yet implemented")) } } @@ -792,12 +894,20 @@ impl TimerService for TimerSvc { &self, req: Request, ) -> Result, Status> { - auth(&self.plugins, &req, "timer.cron")?; - Ok(Response::new(TimerResponse { - timer_id: String::new(), - ok: false, - error: "cron: not yet implemented".to_string(), - })) + let plugin_id = auth(&self.plugins, &req, "timer.cron")?; + let r = req.into_inner(); + match self.kernel.timer.cron(&r.expr, r.callback_id, plugin_id) { + Ok(id) => Ok(Response::new(TimerResponse { + timer_id: id, + ok: true, + error: String::new(), + })), + Err(e) => Ok(Response::new(TimerResponse { + timer_id: String::new(), + ok: false, + error: e, + })), + } } async fn cancel( @@ -921,7 +1031,35 @@ impl PluginBridgeService for BridgeSvc { &self, req: Request, ) -> Result, Status> { + // Require register token when AGENTSD_REGISTER_TOKEN env is set. + let env_token = + REGISTER_TOKEN.get_or_init(|| std::env::var("AGENTSD_REGISTER_TOKEN").ok()); + if let Some(expected) = env_token { + let provided = req + .metadata() + .get("x-register-token") + .and_then(|v| v.to_str().ok()) + .unwrap_or_default(); + if provided != expected.as_str() { + return Ok(Response::new(RegisterResponse { + ok: false, + error: "invalid or missing register token".into(), + session_id: String::new(), + })); + } + } + let r = req.into_inner(); + + // External registrations must never claim plugin_type="system". + if r.plugin_type == "system" { + return Ok(Response::new(RegisterResponse { + ok: false, + error: "external registration of system plugins is not allowed".into(), + session_id: String::new(), + })); + } + let record = PluginRecord { id: r.id, name: r.name, @@ -956,10 +1094,21 @@ impl PluginBridgeService for BridgeSvc { } async fn call(&self, req: Request) -> Result, Status> { + // bridge.call requires a verified, running plugin identity. + let plugin_id = permission::plugin_id_from_metadata(req.metadata()) + .ok_or_else(|| Status::unauthenticated("bridge.call requires x-plugin-id"))?; + permission::require_running_plugin(&self.plugins, &plugin_id)?; let r = req.into_inner(); let fn_name = &r.r#fn; match self.plugins.find_export_record(&r.target, fn_name) { Some(record) => { + if record.status != PluginStatus::Running { + return Ok(Response::new(CallResponse { + result: vec![], + ok: false, + error: format!("plugin '{}' is not running", record.id), + })); + } let endpoint = record.endpoint.as_deref().unwrap_or_default(); if endpoint.is_empty() { return Ok(Response::new(CallResponse { @@ -1002,28 +1151,25 @@ impl PluginBridgeService for BridgeSvc { &self, req: Request, ) -> Result, Status> { - let metadata_plugin_id = permission::plugin_id_from_metadata(req.metadata()); + // Subscriptions are identity-bound: the caller may only subscribe to + // its own event channel, so a verified x-plugin-id is mandatory. + let Some(plugin_id) = permission::plugin_id_from_metadata(req.metadata()) else { + return Err(Status::unauthenticated( + "stream_call requires x-plugin-id metadata", + )); + }; let r = req.into_inner(); let target = r.target; - if let Some(ref metadata_id) = metadata_plugin_id { - if !target.is_empty() && metadata_id != &target { - return Err(Status::permission_denied(format!( - "plugin '{}' cannot subscribe to '{}' events", - metadata_id, target - ))); - } - } - - let plugin_id = metadata_plugin_id.unwrap_or(target); - if plugin_id.is_empty() { - return Err(Status::invalid_argument( - "stream_call requires target or x-plugin-id", - )); + if !target.is_empty() && plugin_id != target { + return Err(Status::permission_denied(format!( + "plugin '{}' cannot subscribe to '{}' events", + plugin_id, target + ))); } permission::require_running_plugin(&self.plugins, &plugin_id)?; - let mut events = self.kernel.callback_bus.subscribe(&plugin_id); + let (mut events, generation) = self.kernel.callback_bus.subscribe(&plugin_id); let (tx, rx) = tokio::sync::mpsc::channel(16); let bus = self.kernel.callback_bus.clone(); let subscribed_plugin_id = plugin_id.clone(); @@ -1043,7 +1189,7 @@ impl PluginBridgeService for BridgeSvc { break; } } - bus.remove(&subscribed_plugin_id); + bus.remove_if_generation(&subscribed_plugin_id, generation); }); Ok(Response::new(tokio_stream::wrappers::ReceiverStream::new( rx, diff --git a/core/agentsd/src/syscall/fs.rs b/core/agentsd/src/syscall/fs.rs index 8916164..bd3b440 100644 --- a/core/agentsd/src/syscall/fs.rs +++ b/core/agentsd/src/syscall/fs.rs @@ -1,6 +1,7 @@ use std::path::Path; use tokio::fs as async_fs; use tokio::io::{AsyncReadExt, AsyncSeekExt}; +use notify::{Watcher, RecursiveMode, EventKind}; const MAX_READ_SIZE: usize = 64 * 1024 * 1024; // 64 MB @@ -106,6 +107,57 @@ impl FsSyscall { } Ok(entries) } + + pub fn watch(&self, path: &str) -> Result, String> { + let (std_tx, std_rx) = std::sync::mpsc::channel::(); + + let mut watcher = notify::recommended_watcher(move |res: Result| { + match res { + Ok(event) => { + let _ = std_tx.send(event); + } + Err(e) => { + tracing::warn!("fs watch notify error: {}", e); + } + } + }).map_err(|e| format!("failed to create watcher: {}", e))?; + + watcher + .watch(std::path::Path::new(path), RecursiveMode::Recursive) + .map_err(|e| format!("failed to watch path '{}': {}", path, e))?; + + let (tx, rx) = tokio::sync::mpsc::channel::(64); + + tokio::task::spawn_blocking(move || { + // Move watcher into this task to keep it alive + let _watcher = watcher; + + while let Ok(event) = std_rx.recv() { + let kind = match event.kind { + EventKind::Create(_) => "create", + EventKind::Modify(_) => "modify", + EventKind::Remove(_) => "remove", + EventKind::Access(_) => continue, + _ => "other", + }; + let path = event + .paths + .first() + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_default(); + let info = FsWatchEventInfo { + path, + kind: kind.to_string(), + }; + if tx.blocking_send(info).is_err() { + // Receiver dropped (gRPC stream closed), stop watching + break; + } + } + }); + + Ok(rx) + } } pub struct FsStatInfo { @@ -121,3 +173,8 @@ pub struct FsEntryInfo { pub is_dir: bool, pub size: i64, } + +pub struct FsWatchEventInfo { + pub path: String, + pub kind: String, +} diff --git a/core/agentsd/src/syscall/network.rs b/core/agentsd/src/syscall/network.rs index 85c5e42..36a2fcb 100644 --- a/core/agentsd/src/syscall/network.rs +++ b/core/agentsd/src/syscall/network.rs @@ -1,12 +1,34 @@ use std::collections::HashMap; use std::net::SocketAddr; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; use std::time::Duration; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::sync::mpsc; +use tokio::task::JoinHandle; + const MAX_RESPONSE_BODY: u64 = 16 * 1024 * 1024; // 16 MB +/// Accepted connection info delivered via the listen channel. +pub struct ConnAccepted { + pub conn_id: String, + pub peer: String, +} + +/// Internal handle for a managed TCP connection. +struct ConnHandle { + writer: Arc>, + reader_task: JoinHandle<()>, + inbound: Option>>, +} + /// Network syscall: remote communication. pub struct NetworkSyscall { client: reqwest::Client, + conns: Arc>>, + next_conn_id: Arc, } impl NetworkSyscall { @@ -18,13 +40,23 @@ impl NetworkSyscall { .redirect(reqwest::redirect::Policy::limited(5)) .build() .unwrap_or_else(|_| reqwest::Client::new()); - Self { client } + Self { + client, + conns: Arc::new(tokio::sync::Mutex::new(HashMap::new())), + next_conn_id: Arc::new(AtomicU64::new(1)), + } + } + + /// Generate the next unique connection ID. + fn alloc_conn_id(&self) -> String { + let id = self.next_conn_id.fetch_add(1, Ordering::Relaxed); + format!("conn_{}", id) } pub async fn available(&self) -> bool { tokio::time::timeout( Duration::from_secs(2), - tokio::net::TcpStream::connect(SocketAddr::from(([8, 8, 8, 8], 53))), + TcpStream::connect(SocketAddr::from(([8, 8, 8, 8], 53))), ) .await .map(|result| result.is_ok()) @@ -91,4 +123,162 @@ impl NetworkSyscall { Ok((status, resp_headers, resp_body.to_vec())) } -} + + /// Open an outbound TCP connection to `addr` (e.g. "host:port"). + /// Returns (conn_id, inbound data receiver). + pub async fn open_conn( + &self, + addr: &str, + _protocol: &str, + ) -> Result<(String, mpsc::Receiver>), String> { + let stream = TcpStream::connect(addr) + .await + .map_err(|e| format!("connect failed: {e}"))?; + let (read_half, write_half) = stream.into_split(); + + let conn_id = self.alloc_conn_id(); + let (tx, rx) = mpsc::channel::>(64); + + let writer = Arc::new(tokio::sync::Mutex::new(write_half)); + let conns = Arc::clone(&self.conns); + let cid = conn_id.clone(); + + let reader_task = tokio::spawn(async move { + let mut read_half = read_half; + let mut buf = vec![0u8; 8192]; + loop { + match read_half.read(&mut buf).await { + Ok(0) => break, + Ok(n) => { + if tx.send(buf[..n].to_vec()).await.is_err() { + break; + } + } + Err(_) => break, + } + } + conns.lock().await.remove(&cid); + }); + + let handle = ConnHandle { + writer, + reader_task, + inbound: None, + }; + self.conns.lock().await.insert(conn_id.clone(), handle); + + Ok((conn_id, rx)) + } + + /// Send data on an existing connection. + pub async fn send(&self, conn_id: &str, data: &[u8]) -> Result<(), String> { + let writer = { + let map = self.conns.lock().await; + let handle = map + .get(conn_id) + .ok_or_else(|| format!("connection not found: {conn_id}"))?; + Arc::clone(&handle.writer) + }; + let mut w = writer.lock().await; + w.write_all(data) + .await + .map_err(|e| format!("write failed: {e}"))?; + w.flush().await.map_err(|e| format!("flush failed: {e}"))?; + Ok(()) + } + + /// Close and remove a connection. + pub async fn close(&self, conn_id: &str) -> Result<(), String> { + let handle = self + .conns + .lock() + .await + .remove(conn_id) + .ok_or_else(|| format!("connection not found: {conn_id}"))?; + handle.reader_task.abort(); + let mut w = handle.writer.lock().await; + let _ = w.shutdown().await; + Ok(()) + } + + /// Start listening on a TCP port. Returns a receiver that yields each + /// accepted connection's id and peer address. + pub async fn listen( + &self, + port: u32, + _protocol: &str, + ) -> Result, String> { + let listener = TcpListener::bind((std::net::Ipv6Addr::LOCALHOST, port as u16)) + .await + .map_err(|e| format!("bind failed: {e}"))?; + + let (accept_tx, accept_rx) = mpsc::channel::(16); + let conns = Arc::clone(&self.conns); + let id_gen = Arc::clone(&self.next_conn_id); + + tokio::spawn(async move { + loop { + let (stream, peer_addr) = match listener.accept().await { + Ok(v) => v, + Err(_) => break, + }; + + let (read_half, write_half) = stream.into_split(); + let conn_id = { + let id = id_gen.fetch_add(1, Ordering::Relaxed); + format!("conn_{}", id) + }; + + let (tx, inbound_rx) = mpsc::channel::>(64); + let writer = Arc::new(tokio::sync::Mutex::new(write_half)); + let conns_inner = Arc::clone(&conns); + let cid = conn_id.clone(); + + let reader_task = tokio::spawn(async move { + let mut read_half = read_half; + let mut buf = vec![0u8; 8192]; + loop { + match read_half.read(&mut buf).await { + Ok(0) => break, + Ok(n) => { + if tx.send(buf[..n].to_vec()).await.is_err() { + break; + } + } + Err(_) => break, + } + } + conns_inner.lock().await.remove(&cid); + }); + + let handle = ConnHandle { + writer, + reader_task, + inbound: Some(inbound_rx), + }; + conns.lock().await.insert(conn_id.clone(), handle); + + let accepted = ConnAccepted { + conn_id, + peer: peer_addr.to_string(), + }; + if accept_tx.send(accepted).await.is_err() { + break; + } + } + }); + + Ok(accept_rx) + } + + /// Take the inbound data receiver for a passively-accepted connection. + /// Returns None if the connection doesn't exist or has no stored receiver. + pub async fn take_inbound( + &self, + conn_id: &str, + ) -> Option>> { + let mut map = self.conns.lock().await; + let handle = map.get_mut(conn_id)?; + handle.inbound.take() + } +} \ No newline at end of file diff --git a/core/agentsd/src/syscall/process.rs b/core/agentsd/src/syscall/process.rs index 6366c58..f242fa9 100644 --- a/core/agentsd/src/syscall/process.rs +++ b/core/agentsd/src/syscall/process.rs @@ -2,7 +2,8 @@ use std::collections::HashMap; use std::sync::Arc; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::process::Command; -use tokio::sync::{mpsc, Mutex, RwLock}; +use tokio::sync::{mpsc, watch, Mutex, RwLock}; +use tokio::time::Duration; /// Per-process managed state, decoupled from the Child handle. struct ManagedProcess { @@ -10,8 +11,8 @@ struct ManagedProcess { status: Arc>, stdin_tx: Option>>, stdout_rx: Arc>>>, - /// Notified when the process exits. Clone the receiver to wait. - exit_notify: Arc, + /// Watch receiver: becomes `true` when process exits. Clone to wait. + exit_rx: watch::Receiver, /// Handle to abort background tasks on kill. _tasks: Vec>, } @@ -19,13 +20,13 @@ struct ManagedProcess { /// Process syscall: spawn and manage subprocesses. /// Uses the real OS PID as the identifier. pub struct ProcessSyscall { - procs: RwLock>>, + procs: Arc>>>, } impl ProcessSyscall { pub fn new() -> Self { Self { - procs: RwLock::new(HashMap::new()), + procs: Arc::new(RwLock::new(HashMap::new())), } } @@ -61,7 +62,7 @@ impl ProcessSyscall { let child_stderr = child.stderr.take(); let status = Arc::new(Mutex::new("running".to_string())); - let exit_notify = Arc::new(tokio::sync::Notify::new()); + let (exit_tx, exit_rx) = watch::channel(false); // Stdin writer task let (stdin_tx, mut stdin_rx) = mpsc::channel::>(64); @@ -111,14 +112,17 @@ impl ProcessSyscall { } }); - // Reaper task: waits for exit and updates status + // Reaper task: waits for exit, updates status, signals watchers, delayed cleanup let status_clone = status.clone(); - let exit_notify_clone = exit_notify.clone(); + let procs_clone = Arc::clone(&self.procs); let reaper_task = tokio::spawn(async move { let exit_status = child.wait().await; let code = exit_status.map(|s| s.code().unwrap_or(-1)).unwrap_or(-1); *status_clone.lock().await = format!("exited({})", code); - exit_notify_clone.notify_waiters(); + let _ = exit_tx.send(true); + // Delayed cleanup: give stdout readers time to drain buffered data + tokio::time::sleep(Duration::from_secs(5)).await; + procs_clone.write().await.remove(&pid); }); let managed = Arc::new(ManagedProcess { @@ -126,7 +130,7 @@ impl ProcessSyscall { status, stdin_tx: Some(stdin_tx), stdout_rx: Arc::new(Mutex::new(stdout_rx)), - exit_notify, + exit_rx, _tasks: vec![stdin_task, stdout_task, stderr_task, reaper_task], }); @@ -136,19 +140,25 @@ impl ProcessSyscall { } pub async fn kill(&self, pid: u64) -> Result<(), String> { - let procs = self.procs.read().await; - let proc = procs - .get(&pid) - .ok_or_else(|| format!("pid {} not found", pid))? - .clone(); - drop(procs); + let proc = { + let procs = self.procs.read().await; + procs + .get(&pid) + .ok_or_else(|| format!("pid {} not found", pid))? + .clone() + }; + + // Set status to killed first + *proc.status.lock().await = "killed".to_string(); // Abort all background tasks (stdin writer, stdout reader, stderr drain, reaper) + // Aborting reaper drops exit_tx, so any wait() blocked on changed() will wake with Err. for task in proc._tasks.iter() { task.abort(); } - *proc.status.lock().await = "killed".to_string(); - proc.exit_notify.notify_waiters(); + + // Remove from map + self.procs.write().await.remove(&pid); // Also send OS kill #[cfg(unix)] @@ -161,46 +171,41 @@ impl ProcessSyscall { // On Windows, aborting the reaper task triggers kill_on_drop on the Child } - self.procs.write().await.remove(&pid); tracing::info!("process killed: pid={}", pid); Ok(()) } pub async fn wait(&self, pid: u64) -> Result { - let procs = self.procs.read().await; - let proc = procs - .get(&pid) - .ok_or_else(|| format!("pid {} not found", pid))? - .clone(); - drop(procs); + let proc = { + let procs = self.procs.read().await; + procs + .get(&pid) + .ok_or_else(|| format!("pid {} not found", pid))? + .clone() + }; - // Check if already exited before waiting - { - let status = proc.status.lock().await; - if status.starts_with("exited") || *status == "killed" { - let s = status.clone(); - drop(status); - self.procs.write().await.remove(&pid); - return parse_exit_code(&s); + // Use watch channel: no race between checking status and waiting. + // watch retains the latest value, so even if send(true) happened before + // we subscribe, borrow_and_update() will see `true` immediately. + let mut rx = proc.exit_rx.clone(); + while !*rx.borrow_and_update() { + if rx.changed().await.is_err() { + break; // sender dropped (e.g. kill aborted reaper) } } - // Wait for exit notification - proc.exit_notify.notified().await; let status = proc.status.lock().await.clone(); - - // Clean up from map - self.procs.write().await.remove(&pid); parse_exit_code(&status) } pub async fn write_stdin(&self, pid: u64, data: &[u8]) -> Result<(), String> { - let procs = self.procs.read().await; - let proc = procs - .get(&pid) - .ok_or_else(|| format!("pid {} not found", pid))? - .clone(); - drop(procs); + let proc = { + let procs = self.procs.read().await; + procs + .get(&pid) + .ok_or_else(|| format!("pid {} not found", pid))? + .clone() + }; if let Some(ref tx) = proc.stdin_tx { tx.send(data.to_vec()) @@ -212,12 +217,13 @@ impl ProcessSyscall { } pub async fn read_stdout(&self, pid: u64, buf: &mut [u8]) -> Result { - let procs = self.procs.read().await; - let proc = procs - .get(&pid) - .ok_or_else(|| format!("pid {} not found", pid))? - .clone(); - drop(procs); + let proc = { + let procs = self.procs.read().await; + procs + .get(&pid) + .ok_or_else(|| format!("pid {} not found", pid))? + .clone() + }; let mut rx = proc.stdout_rx.lock().await; match rx.recv().await { @@ -268,9 +274,13 @@ impl ProcessSyscall { } pub async fn list(&self) -> Vec<(u64, String, String)> { - let procs = self.procs.read().await; - let mut result = Vec::with_capacity(procs.len()); - for (pid, mp) in procs.iter() { + let snapshot: Vec<(u64, Arc)> = { + let procs = self.procs.read().await; + procs.iter().map(|(pid, mp)| (*pid, mp.clone())).collect() + }; + // procs read lock is dropped here + let mut result = Vec::with_capacity(snapshot.len()); + for (pid, mp) in &snapshot { let status = mp.status.lock().await.clone(); result.push((*pid, mp.cmd.clone(), status)); } @@ -288,3 +298,4 @@ fn parse_exit_code(status: &str) -> Result { Ok(-1) } } + diff --git a/core/agentsd/src/syscall/timer.rs b/core/agentsd/src/syscall/timer.rs index 49663d8..999b677 100644 --- a/core/agentsd/src/syscall/timer.rs +++ b/core/agentsd/src/syscall/timer.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::str::FromStr; use std::sync::{Arc, Mutex}; use tokio::time::{self, Duration}; @@ -75,4 +76,53 @@ impl TimerSyscall { .unwrap_or_default() .as_millis() as i64 } + + /// Cron 表达式为 cron crate 7 段格式: sec min hour day-of-month month day-of-week year + pub fn cron(&self, expr: &str, callback_id: String, plugin_id: String) -> Result { + let schedule = cron::Schedule::from_str(expr) + .map_err(|e| format!("invalid cron expression: {e}"))?; + + let timer_id = { + let mut next = self.next_id.lock().unwrap(); + let id = format!("cron_{}", *next); + *next += 1; + id + }; + + let tid = timer_id.clone(); + let cb = callback_id.clone(); + let pid = plugin_id.clone(); + let bus = self.bus.clone(); + let timers = self.timers.clone(); + + let handle = tokio::spawn(async move { + loop { + let next = schedule.upcoming(chrono::Utc).next(); + match next { + Some(t) => { + let dur = (t - chrono::Utc::now()) + .to_std() + .unwrap_or(std::time::Duration::ZERO); + tokio::time::sleep(dur).await; + bus.fire( + &pid, + TimerEvent { + timer_id: tid.clone(), + callback_id: cb.clone(), + }, + ); + } + None => break, + } + } + timers.lock().unwrap().remove(&tid); + }); + + self.timers + .lock() + .unwrap() + .insert(timer_id.clone(), TimerEntry { handle }); + + Ok(timer_id) + } } diff --git a/docs/01-内核系统调用.md b/docs/01-内核系统调用.md index 925aa45..18da565 100644 --- a/docs/01-内核系统调用.md +++ b/docs/01-内核系统调用.md @@ -78,15 +78,18 @@ memory.search(query) -> results **管**:对外连接、监听 | **不管**:本地 IPC(内核 gRPC 总线) ``` -net.request(url, method, body?) -> response -net.connect(addr, protocol) -> conn -net.listen(port, protocol) -> listener -net.send(conn, data) -net.recv(conn) -> data -net.close(conn) -net.available() -> bool +network.request(url, method, headers?, body?) -> response # HTTP 请求 +network.open_conn(addr, protocol) -> stream ConnEvent # TCP 建连,流式推送入站数据 +network.send(conn_id, data) # 向已有连接发数据 +network.close(conn_id) # 关闭连接 +network.listen(port, protocol) -> stream ConnEvent # 监听端口,每 accept 推送 ConnEvent +network.available() -> bool # 网络连通性探测 ``` +gRPC 流式语义:OpenConn/Listen 返回 `stream ConnEvent{conn_id, data, kind}`。 +- 首帧 kind="open"/"accept" 携带 conn_id,后续 kind="data" 携带入站字节,kind="closed" 表示对端关闭。 +- 发送用单独的 `Send` RPC(需传 conn_id),关闭用 `Close` RPC。 + --- ## 6. Process @@ -115,7 +118,7 @@ proc.list() -> [pid] ``` timer.once(delay, callback) -> id # 到期后通过 PluginBridge.StreamCall 投递事件 -timer.cron(expr, callback) -> id +timer.cron(expr, callback) -> id # cron 表达式格式(7段): sec min hour day month weekday year timer.cancel(id) timer.now() -> timestamp ``` diff --git a/docs/02-插件模型.md b/docs/02-插件模型.md index 61935aa..f7fa487 100644 --- a/docs/02-插件模型.md +++ b/docs/02-插件模型.md @@ -177,3 +177,25 @@ message CallRequest { - 系统插件:内核信任,无限制 - 基础插件:按 `syscalls` 白名单精确到方法(如 `fs.list`)校验,也支持 `fs` / `fs.*` / `*` 授权(= seccomp filter) - 标准插件:syscalls + depends 白名单,未声明的一律拒绝 + +### 身份认证 + +所有 syscall 调用**必须**携带 `x-plugin-id` gRPC metadata,否则返回 `UNAUTHENTICATED`。 +这是第一道安全边界——即使 server 监听 TCP,未注册的进程也无法调用任何 syscall。 + +认证流程: +1. 插件通过 `PluginBridge.Register` 注册,获得 `session_id` +2. 后续所有 syscall 请求须在 gRPC metadata 中带 `x-plugin-id: ` +3. 内核校验:plugin_id 存在 → status=Running → syscall 在该插件的白名单内 → 放行 + +### stream_call 身份绑定 + +`PluginBridge.StreamCall` 用于订阅 Timer 等回调事件。安全策略: +- 必须携带 `x-plugin-id`(否则 UNAUTHENTICATED) +- 只能订阅**自己**的事件通道(target 为空或等于自身 plugin_id) +- 尝试订阅其他插件的事件返回 PERMISSION_DENIED + +### 死插件路由保护 + +`PluginBridge.Call` 在路由到目标插件前,校验其 status 为 Running。 +Stopped/Failed 的插件会立即返回错误,而不是等 5 秒连接超时。 diff --git a/docs/03-实施路线.md b/docs/03-实施路线.md index c4342df..80deac4 100644 --- a/docs/03-实施路线.md +++ b/docs/03-实施路线.md @@ -8,24 +8,27 @@ - [x] 实现 Memory syscall(SQLite KV + FTS5 trigram search,默认写入 data/memory.db) - [x] 实现 Input syscall(stdin) - [x] 实现 Display syscall(stdout) -- [x] 实现 Process syscall(spawn/kill/wait/stdin/stdout) +- [x] 实现 Process syscall(spawn/kill/wait/stdin/stdout + watch channel 无竞态 wait) - [x] 实现 Timer syscall(once/cancel/now + 回调事件通道) - [x] 实现 Audio syscall(stub,待系统插件) - [x] 实现 HID syscall(stub,待系统插件) -- [x] 实现 Network syscall(HTTP via curl,待替换) +- [x] 实现 Network syscall(HTTP via reqwest) - [x] Plugin Bridge(注册/调用路由/心跳,注册状态写入 data/plugins.json) - [x] 插件加载:gRPC 握手 + 注册 -- [x] echo 测试插件(Python)验证基础链路 -- [x] ai_test 手动验证插件(Python,默认 disabled/autoload false)覆盖 Display/Memory/FS/Process/Timer/权限拒绝 +- [x] echo 测试插件(Rust)验证基础链路 +- [x] ai_test 手动验证插件(Rust,默认 disabled/autoload false)覆盖 Display/Memory/FS/Process/Timer/权限拒绝 - [x] proto 定义:完整 9 个 service + PluginBridge +- [x] 权限安全:无 x-plugin-id 的请求一律拒绝(UNAUTHENTICATED) +- [x] 死插件路由保护:Bridge.call 校验 PluginStatus::Running -**验证结果**:agentsd 启动 → Python 插件通过 gRPC 注册 → ai_test 调用 Display/Memory/FS/Process/Timer syscall 并验证权限拒绝。 +**验证结果**:agentsd 启动 → Rust 插件通过 gRPC 注册 → ai_test 调用 Display/Memory/FS/Process/Timer syscall 并验证权限拒绝。 -## 阶段 2:完善核心 syscall +## 阶段 2:完善核心 syscall ✅ 部分完成 -- [ ] Network:替换 curl 为 hyper/reqwest 原生 HTTP client -- [ ] FS.Watch:接入 notify-rs 文件监听 -- [ ] Timer.Cron:接入 cron 表达式解析 +- [x] Network:reqwest 原生 HTTP client(阶段 1 已完成) +- [x] Network TCP 流:open_conn/send/close/listen 双向 TCP 通信 +- [x] FS.Watch:接入 notify-rs v7 文件监听(递归) +- [x] Timer.Cron:接入 cron 表达式定时(7 段格式) - [ ] Audio:接入系统音频后端(wasapi/alsa) - [ ] HID:接入屏幕截取(Windows: win32 API) - [ ] Unix socket / Named pipe 传输(替代 TCP) diff --git a/docs/04-项目结构.md b/docs/04-项目结构.md index 548497b..e7987e1 100644 --- a/docs/04-项目结构.md +++ b/docs/04-项目结构.md @@ -51,22 +51,21 @@ Agentsd/ │ ├── agentsd_pb2.py │ └── agentsd_pb2_grpc.py │ - ├── echo/ # 测试插件 + ├── echo/ # 测试插件(Rust) │ ├── plugin.yaml - │ └── echo_plugin.py + │ └── src/main.rs │ - ├── ai_test/ # AI 手动验证插件(enabled=false,autoload=false) + ├── ai_test/ # AI 手动验证插件(Rust,enabled=false,autoload=false) │ ├── plugin.yaml - │ └── ai_test_plugin.py + │ └── src/main.rs │ - ├── hermes/ # Hermes Agent 桥接(63 tool) + ├── hermes/ # Hermes Agent 桥接(Python,Rust 版仅 stub) │ ├── plugin.yaml │ └── hermes_plugin.py │ - └── claudecode/ # Claude Code CLI 桥接 + └── claudecode/ # Claude Code CLI 桥接(Rust) ├── plugin.yaml - ├── claudecode_plugin.py - └── test_chat.py + └── src/main.rs ``` ## 布局原则 @@ -94,7 +93,7 @@ cargo build --release # 约 6.1MB 单二进制 # 启动插件(各开一个终端) python plugins/hermes/hermes_plugin.py -python plugins/claudecode/claudecode_plugin.py +cargo run -p agentsd-plugin-claudecode ``` ## 手动验证插件 @@ -103,7 +102,7 @@ python plugins/claudecode/claudecode_plugin.py ```bash RUST_LOG=agentsd=info ./target/release/agentsd.exe -python plugins/ai_test/ai_test_plugin.py +cargo run -p agentsd-plugin-ai-test ``` ## 重新生成 SDK diff --git a/plugins/_sdk/agentsd_pb2.py b/plugins/_sdk/agentsd_pb2.py index 3bf1d10..39b33f6 100644 --- a/plugins/_sdk/agentsd_pb2.py +++ b/plugins/_sdk/agentsd_pb2.py @@ -24,7 +24,7 @@ _sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\ragentsd.proto\x12\x07\x61gentsd\"%\n\x12\x44isplayTextRequest\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\"4\n\x12\x44isplayRichRequest\x12\x0e\n\x06markup\x18\x01 \x01(\t\x12\x0e\n\x06\x66ormat\x18\x02 \x01(\t\"3\n\x13\x44isplayImageRequest\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x0e\n\x06\x66ormat\x18\x02 \x01(\t\"\'\n\x14\x44isplayNotifyRequest\x12\x0f\n\x07message\x18\x01 \x01(\t\",\n\x0f\x44isplayResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"*\n\nAudioChunk\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x0e\n\x06\x66ormat\x18\x02 \x01(\t\"\x1e\n\rVolumeRequest\x12\r\n\x05level\x18\x01 \x01(\x02\"$\n\x12RecordStartRequest\x12\x0e\n\x06\x66ormat\x18\x01 \x01(\t\")\n\tAudioData\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x0e\n\x06\x66ormat\x18\x02 \x01(\t\"*\n\rAudioResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x1d\n\rFsPathRequest\x12\x0c\n\x04path\x18\x01 \x01(\t\"=\n\rFsReadRequest\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0e\n\x06offset\x18\x02 \x01(\x03\x12\x0e\n\x06length\x18\x03 \x01(\x03\"9\n\x0e\x46sReadResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\n\n\x02ok\x18\x02 \x01(\x08\x12\r\n\x05\x65rror\x18\x03 \x01(\t\"<\n\x0e\x46sWriteRequest\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x0e\n\x06\x61ppend\x18\x03 \x01(\x08\"+\n\x0f\x46sRenameRequest\x12\x0c\n\x04\x66rom\x18\x01 \x01(\t\x12\n\n\x02to\x18\x02 \x01(\t\"\x92\x01\n\x0e\x46sStatResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12\x0c\n\x04size\x18\x03 \x01(\x03\x12\x0e\n\x06is_dir\x18\x04 \x01(\x08\x12\x0f\n\x07is_file\x18\x05 \x01(\x08\x12\x10\n\x08modified\x18\x06 \x01(\x03\x12\x0f\n\x07\x63reated\x18\x07 \x01(\x03\x12\x13\n\x0bpermissions\x18\x08 \x01(\r\"N\n\x0e\x46sListResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12!\n\x07\x65ntries\x18\x03 \x03(\x0b\x32\x10.agentsd.FsEntry\"5\n\x07\x46sEntry\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06is_dir\x18\x02 \x01(\x08\x12\x0c\n\x04size\x18\x03 \x01(\x03\"*\n\x0c\x46sWatchEvent\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\"\'\n\nFsResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x1f\n\x10MemoryKeyRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\" \n\x11MemoryReadRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\"A\n\x12MemoryReadResponse\x12\r\n\x05value\x18\x01 \x01(\x0c\x12\r\n\x05\x66ound\x18\x02 \x01(\x08\x12\r\n\x05\x65rror\x18\x03 \x01(\t\"0\n\x12MemoryWriteRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c\"2\n\x11MemoryListRequest\x12\x0e\n\x06prefix\x18\x01 \x01(\t\x12\r\n\x05limit\x18\x02 \x01(\x05\"1\n\x12MemoryListResponse\x12\x0c\n\x04keys\x18\x01 \x03(\t\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"3\n\x13MemorySearchRequest\x12\r\n\x05query\x18\x01 \x01(\t\x12\r\n\x05limit\x18\x02 \x01(\x05\"M\n\x14MemorySearchResponse\x12&\n\x04hits\x18\x01 \x03(\x0b\x32\x18.agentsd.MemorySearchHit\x12\r\n\x05\x65rror\x18\x02 \x01(\t\">\n\x0fMemorySearchHit\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0f\n\x07snippet\x18\x02 \x01(\t\x12\r\n\x05score\x18\x03 \x01(\x02\"+\n\x0eMemoryResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x9c\x01\n\x0bHttpRequest\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x0e\n\x06method\x18\x02 \x01(\t\x12\x32\n\x07headers\x18\x03 \x03(\x0b\x32!.agentsd.HttpRequest.HeadersEntry\x12\x0c\n\x04\x62ody\x18\x04 \x01(\x0c\x1a.\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xa0\x01\n\x0cHttpResponse\x12\x0e\n\x06status\x18\x01 \x01(\x05\x12\x33\n\x07headers\x18\x02 \x03(\x0b\x32\".agentsd.HttpResponse.HeadersEntry\x12\x0c\n\x04\x62ody\x18\x03 \x01(\x0c\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x1a.\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"0\n\x0e\x43onnectRequest\x12\x0c\n\x04\x61\x64\x64r\x18\x01 \x01(\t\x12\x10\n\x08protocol\x18\x02 \x01(\t\"/\n\rListenRequest\x12\x0c\n\x04port\x18\x01 \x01(\r\x12\x10\n\x08protocol\x18\x02 \x01(\t\"8\n\tConnEvent\x12\x0f\n\x07\x63onn_id\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x0c\n\x04kind\x18\x03 \x01(\t\",\n\x0bSendRequest\x12\x0f\n\x07\x63onn_id\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"\x1f\n\x0c\x43loseRequest\x12\x0f\n\x07\x63onn_id\x18\x01 \x01(\t\"\x19\n\tDataChunk\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"(\n\x0bNetResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\")\n\x14NetAvailableResponse\x12\x11\n\tavailable\x18\x01 \x01(\x08\"\x8f\x01\n\x0cSpawnRequest\x12\x0b\n\x03\x63md\x18\x01 \x01(\t\x12\x0c\n\x04\x61rgs\x18\x02 \x03(\t\x12+\n\x03\x65nv\x18\x03 \x03(\x0b\x32\x1e.agentsd.SpawnRequest.EnvEntry\x12\x0b\n\x03\x63wd\x18\x04 \x01(\t\x1a*\n\x08\x45nvEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"7\n\rSpawnResponse\x12\x0b\n\x03pid\x18\x01 \x01(\x04\x12\n\n\x02ok\x18\x02 \x01(\x08\x12\r\n\x05\x65rror\x18\x03 \x01(\t\"\x19\n\nPidRequest\x12\x0b\n\x03pid\x18\x01 \x01(\x04\"<\n\x0cWaitResponse\x12\x11\n\texit_code\x18\x01 \x01(\x05\x12\n\n\x02ok\x18\x02 \x01(\x08\x12\r\n\x05\x65rror\x18\x03 \x01(\t\")\n\x0cStdinRequest\x12\x0b\n\x03pid\x18\x01 \x01(\x04\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\",\n\rSignalRequest\x12\x0b\n\x03pid\x18\x01 \x01(\x04\x12\x0e\n\x06signal\x18\x02 \x01(\x05\")\n\x0cProcResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"4\n\x10ProcListResponse\x12 \n\x05procs\x18\x01 \x03(\x0b\x32\x11.agentsd.ProcInfo\"4\n\x08ProcInfo\x12\x0b\n\x03pid\x18\x01 \x01(\x04\x12\x0b\n\x03\x63md\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\t\"9\n\x10TimerOnceRequest\x12\x10\n\x08\x64\x65lay_ms\x18\x01 \x01(\x04\x12\x13\n\x0b\x63\x61llback_id\x18\x02 \x01(\t\"5\n\x10TimerCronRequest\x12\x0c\n\x04\x65xpr\x18\x01 \x01(\t\x12\x13\n\x0b\x63\x61llback_id\x18\x02 \x01(\t\"&\n\x12TimerCancelRequest\x12\x10\n\x08timer_id\x18\x01 \x01(\t\"<\n\rTimerResponse\x12\x10\n\x08timer_id\x18\x01 \x01(\t\x12\n\n\x02ok\x18\x02 \x01(\x08\x12\r\n\x05\x65rror\x18\x03 \x01(\t\"(\n\x10TimerNowResponse\x12\x14\n\x0ctimestamp_ms\x18\x01 \x01(\x03\"E\n\x0e\x43\x61ptureRequest\x12\t\n\x01x\x18\x01 \x01(\x05\x12\t\n\x01y\x18\x02 \x01(\x05\x12\r\n\x05width\x18\x03 \x01(\x05\x12\x0e\n\x06height\x18\x04 \x01(\x05\"K\n\x0f\x43\x61ptureResponse\x12\r\n\x05image\x18\x01 \x01(\x0c\x12\x0e\n\x06\x66ormat\x18\x02 \x01(\t\x12\n\n\x02ok\x18\x03 \x01(\x08\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"D\n\x0cMouseRequest\x12\t\n\x01x\x18\x01 \x01(\x05\x12\t\n\x01y\x18\x02 \x01(\x05\x12\x0e\n\x06\x61\x63tion\x18\x03 \x01(\t\x12\x0e\n\x06\x62utton\x18\x04 \x01(\t\"A\n\x0fKeyboardRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0e\n\x06\x61\x63tion\x18\x02 \x01(\t\x12\x11\n\tmodifiers\x18\x03 \x03(\t\"(\n\x0bHidResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"6\n\x0bOcrResponse\x12\x0c\n\x04text\x18\x01 \x01(\t\x12\n\n\x02ok\x18\x02 \x01(\x08\x12\r\n\x05\x65rror\x18\x03 \x01(\t\"<\n\x11InputTextResponse\x12\x0c\n\x04text\x18\x01 \x01(\t\x12\n\n\x02ok\x18\x02 \x01(\x08\x12\r\n\x05\x65rror\x18\x03 \x01(\t\"B\n\x10InputKeyResponse\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0e\n\x06\x61\x63tion\x18\x02 \x01(\t\x12\x11\n\tmodifiers\x18\x03 \x03(\t\"O\n\x16InputClipboardResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x0c\n\x04mime\x18\x02 \x01(\t\x12\n\n\x02ok\x18\x03 \x01(\x08\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"(\n\nInputEvent\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"\x94\x01\n\nPluginInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0f\n\x07version\x18\x03 \x01(\t\x12\x13\n\x0bplugin_type\x18\x04 \x01(\t\x12\x10\n\x08syscalls\x18\x05 \x03(\t\x12\x0f\n\x07\x64\x65pends\x18\x06 \x03(\t\x12#\n\x07\x65xports\x18\x07 \x03(\x0b\x32\x12.agentsd.ExportDef\".\n\tExportDef\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\"A\n\x10RegisterResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12\x12\n\nsession_id\x18\x03 \x01(\t\"7\n\x0b\x43\x61llRequest\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\n\n\x02\x66n\x18\x02 \x01(\t\x12\x0c\n\x04\x61rgs\x18\x03 \x01(\x0c\"9\n\x0c\x43\x61llResponse\x12\x0e\n\x06result\x18\x01 \x01(\x0c\x12\n\n\x02ok\x18\x02 \x01(\x08\x12\r\n\x05\x65rror\x18\x03 \x01(\t\"%\n\x10HeartbeatRequest\x12\x11\n\tplugin_id\x18\x01 \x01(\t\"\x1f\n\x11HeartbeatResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\"\x07\n\x05\x45mpty2\xbe\x02\n\x07\x44isplay\x12=\n\x04Text\x12\x1b.agentsd.DisplayTextRequest\x1a\x18.agentsd.DisplayResponse\x12=\n\x04Rich\x12\x1b.agentsd.DisplayRichRequest\x1a\x18.agentsd.DisplayResponse\x12?\n\x05Image\x12\x1c.agentsd.DisplayImageRequest\x1a\x18.agentsd.DisplayResponse\x12\x31\n\x05\x43lear\x12\x0e.agentsd.Empty\x1a\x18.agentsd.DisplayResponse\x12\x41\n\x06Notify\x12\x1d.agentsd.DisplayNotifyRequest\x1a\x18.agentsd.DisplayResponse2\xe2\x02\n\x05\x41udio\x12\x35\n\x04Play\x12\x13.agentsd.AudioChunk\x1a\x16.agentsd.AudioResponse(\x01\x12.\n\x04Stop\x12\x0e.agentsd.Empty\x1a\x16.agentsd.AudioResponse\x12\x38\n\x06Volume\x12\x16.agentsd.VolumeRequest\x1a\x16.agentsd.AudioResponse\x12\x42\n\x0bRecordStart\x12\x1b.agentsd.RecordStartRequest\x1a\x16.agentsd.AudioResponse\x12\x30\n\nRecordStop\x12\x0e.agentsd.Empty\x1a\x12.agentsd.AudioData\x12\x42\n\x0cRecordStream\x12\x1b.agentsd.RecordStartRequest\x1a\x13.agentsd.AudioChunk0\x01\x32\x90\x03\n\x02\x46S\x12\x37\n\x04Read\x12\x16.agentsd.FsReadRequest\x1a\x17.agentsd.FsReadResponse\x12\x35\n\x05Write\x12\x17.agentsd.FsWriteRequest\x1a\x13.agentsd.FsResponse\x12\x35\n\x06\x44\x65lete\x12\x16.agentsd.FsPathRequest\x1a\x13.agentsd.FsResponse\x12\x37\n\x06Rename\x12\x18.agentsd.FsRenameRequest\x1a\x13.agentsd.FsResponse\x12\x37\n\x04Stat\x12\x16.agentsd.FsPathRequest\x1a\x17.agentsd.FsStatResponse\x12\x37\n\x04List\x12\x16.agentsd.FsPathRequest\x1a\x17.agentsd.FsListResponse\x12\x38\n\x05Watch\x12\x16.agentsd.FsPathRequest\x1a\x15.agentsd.FsWatchEvent0\x01\x32\xce\x02\n\x06Memory\x12?\n\x04Read\x12\x1a.agentsd.MemoryReadRequest\x1a\x1b.agentsd.MemoryReadResponse\x12=\n\x05Write\x12\x1b.agentsd.MemoryWriteRequest\x1a\x17.agentsd.MemoryResponse\x12<\n\x06\x44\x65lete\x12\x19.agentsd.MemoryKeyRequest\x1a\x17.agentsd.MemoryResponse\x12?\n\x04List\x12\x1a.agentsd.MemoryListRequest\x1a\x1b.agentsd.MemoryListResponse\x12\x45\n\x06Search\x12\x1c.agentsd.MemorySearchRequest\x1a\x1d.agentsd.MemorySearchResponse2\xda\x02\n\x07Network\x12\x36\n\x07Request\x12\x14.agentsd.HttpRequest\x1a\x15.agentsd.HttpResponse\x12\x39\n\x08OpenConn\x12\x17.agentsd.ConnectRequest\x1a\x12.agentsd.DataChunk0\x01\x12\x36\n\x06Listen\x12\x16.agentsd.ListenRequest\x1a\x12.agentsd.ConnEvent0\x01\x12\x32\n\x04Send\x12\x14.agentsd.SendRequest\x1a\x14.agentsd.NetResponse\x12\x34\n\x05\x43lose\x12\x15.agentsd.CloseRequest\x1a\x14.agentsd.NetResponse\x12:\n\tAvailable\x12\x0e.agentsd.Empty\x1a\x1d.agentsd.NetAvailableResponse2\x85\x03\n\x07Process\x12\x36\n\x05Spawn\x12\x15.agentsd.SpawnRequest\x1a\x16.agentsd.SpawnResponse\x12\x32\n\x04Kill\x12\x13.agentsd.PidRequest\x1a\x15.agentsd.ProcResponse\x12\x32\n\x04Wait\x12\x13.agentsd.PidRequest\x1a\x15.agentsd.WaitResponse\x12\x35\n\x05Stdin\x12\x15.agentsd.StdinRequest\x1a\x15.agentsd.ProcResponse\x12\x33\n\x06Stdout\x12\x13.agentsd.PidRequest\x1a\x12.agentsd.DataChunk0\x01\x12\x37\n\x06Signal\x12\x16.agentsd.SignalRequest\x1a\x15.agentsd.ProcResponse\x12\x35\n\x08ListProc\x12\x0e.agentsd.Empty\x1a\x19.agentsd.ProcListResponse2\xee\x01\n\x05Timer\x12\x39\n\x04Once\x12\x19.agentsd.TimerOnceRequest\x1a\x16.agentsd.TimerResponse\x12\x39\n\x04\x43ron\x12\x19.agentsd.TimerCronRequest\x1a\x16.agentsd.TimerResponse\x12=\n\x06\x43\x61ncel\x12\x1b.agentsd.TimerCancelRequest\x1a\x16.agentsd.TimerResponse\x12\x30\n\x03Now\x12\x0e.agentsd.Empty\x1a\x19.agentsd.TimerNowResponse2\xeb\x01\n\x03HID\x12<\n\x07\x43\x61pture\x12\x17.agentsd.CaptureRequest\x1a\x18.agentsd.CaptureResponse\x12\x34\n\x05Mouse\x12\x15.agentsd.MouseRequest\x1a\x14.agentsd.HidResponse\x12:\n\x08Keyboard\x12\x18.agentsd.KeyboardRequest\x1a\x14.agentsd.HidResponse\x12\x34\n\x03Ocr\x12\x17.agentsd.CaptureRequest\x1a\x14.agentsd.OcrResponse2\xdc\x01\n\x05Input\x12\x32\n\x04Text\x12\x0e.agentsd.Empty\x1a\x1a.agentsd.InputTextResponse\x12\x30\n\x03Key\x12\x0e.agentsd.Empty\x1a\x19.agentsd.InputKeyResponse\x12<\n\tClipboard\x12\x0e.agentsd.Empty\x1a\x1f.agentsd.InputClipboardResponse\x12/\n\x06\x45vents\x12\x0e.agentsd.Empty\x1a\x13.agentsd.InputEvent0\x01\x32\x80\x02\n\x0cPluginBridge\x12:\n\x08Register\x12\x13.agentsd.PluginInfo\x1a\x19.agentsd.RegisterResponse\x12\x33\n\x04\x43\x61ll\x12\x14.agentsd.CallRequest\x1a\x15.agentsd.CallResponse\x12;\n\nStreamCall\x12\x14.agentsd.CallRequest\x1a\x15.agentsd.CallResponse0\x01\x12\x42\n\tHeartbeat\x12\x19.agentsd.HeartbeatRequest\x1a\x1a.agentsd.HeartbeatResponseb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\ragentsd.proto\x12\x07\x61gentsd\"%\n\x12\x44isplayTextRequest\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\"4\n\x12\x44isplayRichRequest\x12\x0e\n\x06markup\x18\x01 \x01(\t\x12\x0e\n\x06\x66ormat\x18\x02 \x01(\t\"3\n\x13\x44isplayImageRequest\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x0e\n\x06\x66ormat\x18\x02 \x01(\t\"\'\n\x14\x44isplayNotifyRequest\x12\x0f\n\x07message\x18\x01 \x01(\t\",\n\x0f\x44isplayResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"*\n\nAudioChunk\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x0e\n\x06\x66ormat\x18\x02 \x01(\t\"\x1e\n\rVolumeRequest\x12\r\n\x05level\x18\x01 \x01(\x02\"$\n\x12RecordStartRequest\x12\x0e\n\x06\x66ormat\x18\x01 \x01(\t\")\n\tAudioData\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x0e\n\x06\x66ormat\x18\x02 \x01(\t\"*\n\rAudioResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x1d\n\rFsPathRequest\x12\x0c\n\x04path\x18\x01 \x01(\t\"=\n\rFsReadRequest\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0e\n\x06offset\x18\x02 \x01(\x03\x12\x0e\n\x06length\x18\x03 \x01(\x03\"9\n\x0e\x46sReadResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\n\n\x02ok\x18\x02 \x01(\x08\x12\r\n\x05\x65rror\x18\x03 \x01(\t\"<\n\x0e\x46sWriteRequest\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x0e\n\x06\x61ppend\x18\x03 \x01(\x08\"+\n\x0f\x46sRenameRequest\x12\x0c\n\x04\x66rom\x18\x01 \x01(\t\x12\n\n\x02to\x18\x02 \x01(\t\"\x92\x01\n\x0e\x46sStatResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12\x0c\n\x04size\x18\x03 \x01(\x03\x12\x0e\n\x06is_dir\x18\x04 \x01(\x08\x12\x0f\n\x07is_file\x18\x05 \x01(\x08\x12\x10\n\x08modified\x18\x06 \x01(\x03\x12\x0f\n\x07\x63reated\x18\x07 \x01(\x03\x12\x13\n\x0bpermissions\x18\x08 \x01(\r\"N\n\x0e\x46sListResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12!\n\x07\x65ntries\x18\x03 \x03(\x0b\x32\x10.agentsd.FsEntry\"5\n\x07\x46sEntry\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06is_dir\x18\x02 \x01(\x08\x12\x0c\n\x04size\x18\x03 \x01(\x03\"*\n\x0c\x46sWatchEvent\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\"\'\n\nFsResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x1f\n\x10MemoryKeyRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\" \n\x11MemoryReadRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\"A\n\x12MemoryReadResponse\x12\r\n\x05value\x18\x01 \x01(\x0c\x12\r\n\x05\x66ound\x18\x02 \x01(\x08\x12\r\n\x05\x65rror\x18\x03 \x01(\t\"0\n\x12MemoryWriteRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c\"2\n\x11MemoryListRequest\x12\x0e\n\x06prefix\x18\x01 \x01(\t\x12\r\n\x05limit\x18\x02 \x01(\x05\"1\n\x12MemoryListResponse\x12\x0c\n\x04keys\x18\x01 \x03(\t\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"3\n\x13MemorySearchRequest\x12\r\n\x05query\x18\x01 \x01(\t\x12\r\n\x05limit\x18\x02 \x01(\x05\"M\n\x14MemorySearchResponse\x12&\n\x04hits\x18\x01 \x03(\x0b\x32\x18.agentsd.MemorySearchHit\x12\r\n\x05\x65rror\x18\x02 \x01(\t\">\n\x0fMemorySearchHit\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0f\n\x07snippet\x18\x02 \x01(\t\x12\r\n\x05score\x18\x03 \x01(\x02\"+\n\x0eMemoryResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x9c\x01\n\x0bHttpRequest\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x0e\n\x06method\x18\x02 \x01(\t\x12\x32\n\x07headers\x18\x03 \x03(\x0b\x32!.agentsd.HttpRequest.HeadersEntry\x12\x0c\n\x04\x62ody\x18\x04 \x01(\x0c\x1a.\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xa0\x01\n\x0cHttpResponse\x12\x0e\n\x06status\x18\x01 \x01(\x05\x12\x33\n\x07headers\x18\x02 \x03(\x0b\x32\".agentsd.HttpResponse.HeadersEntry\x12\x0c\n\x04\x62ody\x18\x03 \x01(\x0c\x12\r\n\x05\x65rror\x18\x04 \x01(\t\x1a.\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"0\n\x0e\x43onnectRequest\x12\x0c\n\x04\x61\x64\x64r\x18\x01 \x01(\t\x12\x10\n\x08protocol\x18\x02 \x01(\t\"/\n\rListenRequest\x12\x0c\n\x04port\x18\x01 \x01(\r\x12\x10\n\x08protocol\x18\x02 \x01(\t\"8\n\tConnEvent\x12\x0f\n\x07\x63onn_id\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x0c\n\x04kind\x18\x03 \x01(\t\",\n\x0bSendRequest\x12\x0f\n\x07\x63onn_id\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"\x1f\n\x0c\x43loseRequest\x12\x0f\n\x07\x63onn_id\x18\x01 \x01(\t\"\x19\n\tDataChunk\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"(\n\x0bNetResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\")\n\x14NetAvailableResponse\x12\x11\n\tavailable\x18\x01 \x01(\x08\"\x8f\x01\n\x0cSpawnRequest\x12\x0b\n\x03\x63md\x18\x01 \x01(\t\x12\x0c\n\x04\x61rgs\x18\x02 \x03(\t\x12+\n\x03\x65nv\x18\x03 \x03(\x0b\x32\x1e.agentsd.SpawnRequest.EnvEntry\x12\x0b\n\x03\x63wd\x18\x04 \x01(\t\x1a*\n\x08\x45nvEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"7\n\rSpawnResponse\x12\x0b\n\x03pid\x18\x01 \x01(\x04\x12\n\n\x02ok\x18\x02 \x01(\x08\x12\r\n\x05\x65rror\x18\x03 \x01(\t\"\x19\n\nPidRequest\x12\x0b\n\x03pid\x18\x01 \x01(\x04\"<\n\x0cWaitResponse\x12\x11\n\texit_code\x18\x01 \x01(\x05\x12\n\n\x02ok\x18\x02 \x01(\x08\x12\r\n\x05\x65rror\x18\x03 \x01(\t\")\n\x0cStdinRequest\x12\x0b\n\x03pid\x18\x01 \x01(\x04\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\",\n\rSignalRequest\x12\x0b\n\x03pid\x18\x01 \x01(\x04\x12\x0e\n\x06signal\x18\x02 \x01(\x05\")\n\x0cProcResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"4\n\x10ProcListResponse\x12 \n\x05procs\x18\x01 \x03(\x0b\x32\x11.agentsd.ProcInfo\"4\n\x08ProcInfo\x12\x0b\n\x03pid\x18\x01 \x01(\x04\x12\x0b\n\x03\x63md\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\t\"9\n\x10TimerOnceRequest\x12\x10\n\x08\x64\x65lay_ms\x18\x01 \x01(\x04\x12\x13\n\x0b\x63\x61llback_id\x18\x02 \x01(\t\"5\n\x10TimerCronRequest\x12\x0c\n\x04\x65xpr\x18\x01 \x01(\t\x12\x13\n\x0b\x63\x61llback_id\x18\x02 \x01(\t\"&\n\x12TimerCancelRequest\x12\x10\n\x08timer_id\x18\x01 \x01(\t\"<\n\rTimerResponse\x12\x10\n\x08timer_id\x18\x01 \x01(\t\x12\n\n\x02ok\x18\x02 \x01(\x08\x12\r\n\x05\x65rror\x18\x03 \x01(\t\"(\n\x10TimerNowResponse\x12\x14\n\x0ctimestamp_ms\x18\x01 \x01(\x03\"E\n\x0e\x43\x61ptureRequest\x12\t\n\x01x\x18\x01 \x01(\x05\x12\t\n\x01y\x18\x02 \x01(\x05\x12\r\n\x05width\x18\x03 \x01(\x05\x12\x0e\n\x06height\x18\x04 \x01(\x05\"K\n\x0f\x43\x61ptureResponse\x12\r\n\x05image\x18\x01 \x01(\x0c\x12\x0e\n\x06\x66ormat\x18\x02 \x01(\t\x12\n\n\x02ok\x18\x03 \x01(\x08\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"D\n\x0cMouseRequest\x12\t\n\x01x\x18\x01 \x01(\x05\x12\t\n\x01y\x18\x02 \x01(\x05\x12\x0e\n\x06\x61\x63tion\x18\x03 \x01(\t\x12\x0e\n\x06\x62utton\x18\x04 \x01(\t\"A\n\x0fKeyboardRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0e\n\x06\x61\x63tion\x18\x02 \x01(\t\x12\x11\n\tmodifiers\x18\x03 \x03(\t\"(\n\x0bHidResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"6\n\x0bOcrResponse\x12\x0c\n\x04text\x18\x01 \x01(\t\x12\n\n\x02ok\x18\x02 \x01(\x08\x12\r\n\x05\x65rror\x18\x03 \x01(\t\"<\n\x11InputTextResponse\x12\x0c\n\x04text\x18\x01 \x01(\t\x12\n\n\x02ok\x18\x02 \x01(\x08\x12\r\n\x05\x65rror\x18\x03 \x01(\t\"B\n\x10InputKeyResponse\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0e\n\x06\x61\x63tion\x18\x02 \x01(\t\x12\x11\n\tmodifiers\x18\x03 \x03(\t\"O\n\x16InputClipboardResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x0c\n\x04mime\x18\x02 \x01(\t\x12\n\n\x02ok\x18\x03 \x01(\x08\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"(\n\nInputEvent\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"\xa6\x01\n\nPluginInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0f\n\x07version\x18\x03 \x01(\t\x12\x13\n\x0bplugin_type\x18\x04 \x01(\t\x12\x10\n\x08syscalls\x18\x05 \x03(\t\x12\x0f\n\x07\x64\x65pends\x18\x06 \x03(\t\x12#\n\x07\x65xports\x18\x07 \x03(\x0b\x32\x12.agentsd.ExportDef\x12\x10\n\x08\x65ndpoint\x18\x08 \x01(\t\".\n\tExportDef\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\"A\n\x10RegisterResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12\x12\n\nsession_id\x18\x03 \x01(\t\"7\n\x0b\x43\x61llRequest\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\n\n\x02\x66n\x18\x02 \x01(\t\x12\x0c\n\x04\x61rgs\x18\x03 \x01(\x0c\"9\n\x0c\x43\x61llResponse\x12\x0e\n\x06result\x18\x01 \x01(\x0c\x12\n\n\x02ok\x18\x02 \x01(\x08\x12\r\n\x05\x65rror\x18\x03 \x01(\t\"%\n\x10HeartbeatRequest\x12\x11\n\tplugin_id\x18\x01 \x01(\t\"\x1f\n\x11HeartbeatResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\"\x07\n\x05\x45mpty2\xbe\x02\n\x07\x44isplay\x12=\n\x04Text\x12\x1b.agentsd.DisplayTextRequest\x1a\x18.agentsd.DisplayResponse\x12=\n\x04Rich\x12\x1b.agentsd.DisplayRichRequest\x1a\x18.agentsd.DisplayResponse\x12?\n\x05Image\x12\x1c.agentsd.DisplayImageRequest\x1a\x18.agentsd.DisplayResponse\x12\x31\n\x05\x43lear\x12\x0e.agentsd.Empty\x1a\x18.agentsd.DisplayResponse\x12\x41\n\x06Notify\x12\x1d.agentsd.DisplayNotifyRequest\x1a\x18.agentsd.DisplayResponse2\xe2\x02\n\x05\x41udio\x12\x35\n\x04Play\x12\x13.agentsd.AudioChunk\x1a\x16.agentsd.AudioResponse(\x01\x12.\n\x04Stop\x12\x0e.agentsd.Empty\x1a\x16.agentsd.AudioResponse\x12\x38\n\x06Volume\x12\x16.agentsd.VolumeRequest\x1a\x16.agentsd.AudioResponse\x12\x42\n\x0bRecordStart\x12\x1b.agentsd.RecordStartRequest\x1a\x16.agentsd.AudioResponse\x12\x30\n\nRecordStop\x12\x0e.agentsd.Empty\x1a\x12.agentsd.AudioData\x12\x42\n\x0cRecordStream\x12\x1b.agentsd.RecordStartRequest\x1a\x13.agentsd.AudioChunk0\x01\x32\x90\x03\n\x02\x46S\x12\x37\n\x04Read\x12\x16.agentsd.FsReadRequest\x1a\x17.agentsd.FsReadResponse\x12\x35\n\x05Write\x12\x17.agentsd.FsWriteRequest\x1a\x13.agentsd.FsResponse\x12\x35\n\x06\x44\x65lete\x12\x16.agentsd.FsPathRequest\x1a\x13.agentsd.FsResponse\x12\x37\n\x06Rename\x12\x18.agentsd.FsRenameRequest\x1a\x13.agentsd.FsResponse\x12\x37\n\x04Stat\x12\x16.agentsd.FsPathRequest\x1a\x17.agentsd.FsStatResponse\x12\x37\n\x04List\x12\x16.agentsd.FsPathRequest\x1a\x17.agentsd.FsListResponse\x12\x38\n\x05Watch\x12\x16.agentsd.FsPathRequest\x1a\x15.agentsd.FsWatchEvent0\x01\x32\xce\x02\n\x06Memory\x12?\n\x04Read\x12\x1a.agentsd.MemoryReadRequest\x1a\x1b.agentsd.MemoryReadResponse\x12=\n\x05Write\x12\x1b.agentsd.MemoryWriteRequest\x1a\x17.agentsd.MemoryResponse\x12<\n\x06\x44\x65lete\x12\x19.agentsd.MemoryKeyRequest\x1a\x17.agentsd.MemoryResponse\x12?\n\x04List\x12\x1a.agentsd.MemoryListRequest\x1a\x1b.agentsd.MemoryListResponse\x12\x45\n\x06Search\x12\x1c.agentsd.MemorySearchRequest\x1a\x1d.agentsd.MemorySearchResponse2\xda\x02\n\x07Network\x12\x36\n\x07Request\x12\x14.agentsd.HttpRequest\x1a\x15.agentsd.HttpResponse\x12\x39\n\x08OpenConn\x12\x17.agentsd.ConnectRequest\x1a\x12.agentsd.ConnEvent0\x01\x12\x36\n\x06Listen\x12\x16.agentsd.ListenRequest\x1a\x12.agentsd.ConnEvent0\x01\x12\x32\n\x04Send\x12\x14.agentsd.SendRequest\x1a\x14.agentsd.NetResponse\x12\x34\n\x05\x43lose\x12\x15.agentsd.CloseRequest\x1a\x14.agentsd.NetResponse\x12:\n\tAvailable\x12\x0e.agentsd.Empty\x1a\x1d.agentsd.NetAvailableResponse2\x85\x03\n\x07Process\x12\x36\n\x05Spawn\x12\x15.agentsd.SpawnRequest\x1a\x16.agentsd.SpawnResponse\x12\x32\n\x04Kill\x12\x13.agentsd.PidRequest\x1a\x15.agentsd.ProcResponse\x12\x32\n\x04Wait\x12\x13.agentsd.PidRequest\x1a\x15.agentsd.WaitResponse\x12\x35\n\x05Stdin\x12\x15.agentsd.StdinRequest\x1a\x15.agentsd.ProcResponse\x12\x33\n\x06Stdout\x12\x13.agentsd.PidRequest\x1a\x12.agentsd.DataChunk0\x01\x12\x37\n\x06Signal\x12\x16.agentsd.SignalRequest\x1a\x15.agentsd.ProcResponse\x12\x35\n\x08ListProc\x12\x0e.agentsd.Empty\x1a\x19.agentsd.ProcListResponse2\xee\x01\n\x05Timer\x12\x39\n\x04Once\x12\x19.agentsd.TimerOnceRequest\x1a\x16.agentsd.TimerResponse\x12\x39\n\x04\x43ron\x12\x19.agentsd.TimerCronRequest\x1a\x16.agentsd.TimerResponse\x12=\n\x06\x43\x61ncel\x12\x1b.agentsd.TimerCancelRequest\x1a\x16.agentsd.TimerResponse\x12\x30\n\x03Now\x12\x0e.agentsd.Empty\x1a\x19.agentsd.TimerNowResponse2\xeb\x01\n\x03HID\x12<\n\x07\x43\x61pture\x12\x17.agentsd.CaptureRequest\x1a\x18.agentsd.CaptureResponse\x12\x34\n\x05Mouse\x12\x15.agentsd.MouseRequest\x1a\x14.agentsd.HidResponse\x12:\n\x08Keyboard\x12\x18.agentsd.KeyboardRequest\x1a\x14.agentsd.HidResponse\x12\x34\n\x03Ocr\x12\x17.agentsd.CaptureRequest\x1a\x14.agentsd.OcrResponse2\xdc\x01\n\x05Input\x12\x32\n\x04Text\x12\x0e.agentsd.Empty\x1a\x1a.agentsd.InputTextResponse\x12\x30\n\x03Key\x12\x0e.agentsd.Empty\x1a\x19.agentsd.InputKeyResponse\x12<\n\tClipboard\x12\x0e.agentsd.Empty\x1a\x1f.agentsd.InputClipboardResponse\x12/\n\x06\x45vents\x12\x0e.agentsd.Empty\x1a\x13.agentsd.InputEvent0\x01\x32\x80\x02\n\x0cPluginBridge\x12:\n\x08Register\x12\x13.agentsd.PluginInfo\x1a\x19.agentsd.RegisterResponse\x12\x33\n\x04\x43\x61ll\x12\x14.agentsd.CallRequest\x1a\x15.agentsd.CallResponse\x12;\n\nStreamCall\x12\x14.agentsd.CallRequest\x1a\x15.agentsd.CallResponse0\x01\x12\x42\n\tHeartbeat\x12\x19.agentsd.HeartbeatRequest\x1a\x1a.agentsd.HeartbeatResponseb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -172,39 +172,39 @@ if not _descriptor._USE_C_DESCRIPTORS: _globals['_INPUTEVENT']._serialized_start=3671 _globals['_INPUTEVENT']._serialized_end=3711 _globals['_PLUGININFO']._serialized_start=3714 - _globals['_PLUGININFO']._serialized_end=3862 - _globals['_EXPORTDEF']._serialized_start=3864 - _globals['_EXPORTDEF']._serialized_end=3910 - _globals['_REGISTERRESPONSE']._serialized_start=3912 - _globals['_REGISTERRESPONSE']._serialized_end=3977 - _globals['_CALLREQUEST']._serialized_start=3979 - _globals['_CALLREQUEST']._serialized_end=4034 - _globals['_CALLRESPONSE']._serialized_start=4036 - _globals['_CALLRESPONSE']._serialized_end=4093 - _globals['_HEARTBEATREQUEST']._serialized_start=4095 - _globals['_HEARTBEATREQUEST']._serialized_end=4132 - _globals['_HEARTBEATRESPONSE']._serialized_start=4134 - _globals['_HEARTBEATRESPONSE']._serialized_end=4165 - _globals['_EMPTY']._serialized_start=4167 - _globals['_EMPTY']._serialized_end=4174 - _globals['_DISPLAY']._serialized_start=4177 - _globals['_DISPLAY']._serialized_end=4495 - _globals['_AUDIO']._serialized_start=4498 - _globals['_AUDIO']._serialized_end=4852 - _globals['_FS']._serialized_start=4855 - _globals['_FS']._serialized_end=5255 - _globals['_MEMORY']._serialized_start=5258 - _globals['_MEMORY']._serialized_end=5592 - _globals['_NETWORK']._serialized_start=5595 - _globals['_NETWORK']._serialized_end=5941 - _globals['_PROCESS']._serialized_start=5944 - _globals['_PROCESS']._serialized_end=6333 - _globals['_TIMER']._serialized_start=6336 - _globals['_TIMER']._serialized_end=6574 - _globals['_HID']._serialized_start=6577 - _globals['_HID']._serialized_end=6812 - _globals['_INPUT']._serialized_start=6815 - _globals['_INPUT']._serialized_end=7035 - _globals['_PLUGINBRIDGE']._serialized_start=7038 - _globals['_PLUGINBRIDGE']._serialized_end=7294 + _globals['_PLUGININFO']._serialized_end=3880 + _globals['_EXPORTDEF']._serialized_start=3882 + _globals['_EXPORTDEF']._serialized_end=3928 + _globals['_REGISTERRESPONSE']._serialized_start=3930 + _globals['_REGISTERRESPONSE']._serialized_end=3995 + _globals['_CALLREQUEST']._serialized_start=3997 + _globals['_CALLREQUEST']._serialized_end=4052 + _globals['_CALLRESPONSE']._serialized_start=4054 + _globals['_CALLRESPONSE']._serialized_end=4111 + _globals['_HEARTBEATREQUEST']._serialized_start=4113 + _globals['_HEARTBEATREQUEST']._serialized_end=4150 + _globals['_HEARTBEATRESPONSE']._serialized_start=4152 + _globals['_HEARTBEATRESPONSE']._serialized_end=4183 + _globals['_EMPTY']._serialized_start=4185 + _globals['_EMPTY']._serialized_end=4192 + _globals['_DISPLAY']._serialized_start=4195 + _globals['_DISPLAY']._serialized_end=4513 + _globals['_AUDIO']._serialized_start=4516 + _globals['_AUDIO']._serialized_end=4870 + _globals['_FS']._serialized_start=4873 + _globals['_FS']._serialized_end=5273 + _globals['_MEMORY']._serialized_start=5276 + _globals['_MEMORY']._serialized_end=5610 + _globals['_NETWORK']._serialized_start=5613 + _globals['_NETWORK']._serialized_end=5959 + _globals['_PROCESS']._serialized_start=5962 + _globals['_PROCESS']._serialized_end=6351 + _globals['_TIMER']._serialized_start=6354 + _globals['_TIMER']._serialized_end=6592 + _globals['_HID']._serialized_start=6595 + _globals['_HID']._serialized_end=6830 + _globals['_INPUT']._serialized_start=6833 + _globals['_INPUT']._serialized_end=7053 + _globals['_PLUGINBRIDGE']._serialized_start=7056 + _globals['_PLUGINBRIDGE']._serialized_end=7312 # @@protoc_insertion_point(module_scope) diff --git a/plugins/_sdk/agentsd_pb2_grpc.py b/plugins/_sdk/agentsd_pb2_grpc.py index e2ef646..899ec52 100644 --- a/plugins/_sdk/agentsd_pb2_grpc.py +++ b/plugins/_sdk/agentsd_pb2_grpc.py @@ -1160,7 +1160,7 @@ class NetworkStub: self.OpenConn = channel.unary_stream( '/agentsd.Network/OpenConn', request_serializer=agentsd__pb2.ConnectRequest.SerializeToString, - response_deserializer=agentsd__pb2.DataChunk.FromString, + response_deserializer=agentsd__pb2.ConnEvent.FromString, _registered_method=True) self.Listen = channel.unary_stream( '/agentsd.Network/Listen', @@ -1235,7 +1235,7 @@ def add_NetworkServicer_to_server(servicer, server): 'OpenConn': grpc.unary_stream_rpc_method_handler( servicer.OpenConn, request_deserializer=agentsd__pb2.ConnectRequest.FromString, - response_serializer=agentsd__pb2.DataChunk.SerializeToString, + response_serializer=agentsd__pb2.ConnEvent.SerializeToString, ), 'Listen': grpc.unary_stream_rpc_method_handler( servicer.Listen, @@ -1312,7 +1312,7 @@ class Network: target, '/agentsd.Network/OpenConn', agentsd__pb2.ConnectRequest.SerializeToString, - agentsd__pb2.DataChunk.FromString, + agentsd__pb2.ConnEvent.FromString, options, channel_credentials, insecure, diff --git a/plugins/ai_test/ai_test_plugin.py b/plugins/ai_test/ai_test_plugin.py deleted file mode 100644 index c717135..0000000 --- a/plugins/ai_test/ai_test_plugin.py +++ /dev/null @@ -1,222 +0,0 @@ -""" -AI Test Plugin for Agentsd -========================== - -Manual verification plugin for AI agents and humans. It is intentionally -not autoloaded by default; run it only when testing Agentsd runtime behavior. - -Usage: - # Start agentsd first: - # RUST_LOG=agentsd=info ./target/release/agentsd.exe - python plugins/ai_test/ai_test_plugin.py -""" - -import json -import sys -import time -from concurrent.futures import ThreadPoolExecutor, TimeoutError -from pathlib import Path - -import grpc - -ROOT = Path(__file__).resolve().parents[2] -SDK_DIR = ROOT / "plugins" / "_sdk" -sys.path.insert(0, str(SDK_DIR)) - -import agentsd_pb2 as pb # noqa: E402 -import agentsd_pb2_grpc as rpc # noqa: E402 - -PLUGIN_ID = "ai-test" -METADATA = (("x-plugin-id", PLUGIN_ID),) -ADDR = "localhost:50051" - - -class CheckRunner: - def __init__(self): - self.channel = grpc.insecure_channel(ADDR) - self.bridge = rpc.PluginBridgeStub(self.channel) - self.display = rpc.DisplayStub(self.channel) - self.memory = rpc.MemoryStub(self.channel) - self.fs = rpc.FSStub(self.channel) - self.process = rpc.ProcessStub(self.channel) - self.timer = rpc.TimerStub(self.channel) - self.network = rpc.NetworkStub(self.channel) - self.results = [] - self.run_id = str(int(time.time() * 1000)) - self.scratch_dir = ROOT / "target" / "release" / "data" / "ai_test" / self.run_id - - def close(self): - self.channel.close() - - def record(self, name, ok, details=""): - icon = "PASS" if ok else "FAIL" - print(f"[{icon}] {name}: {details}") - self.results.append({"name": name, "ok": bool(ok), "details": details}) - - def check(self, name, fn): - try: - details = fn() - self.record(name, True, details or "ok") - except Exception as exc: - self.record(name, False, str(exc)) - - def register(self): - resp = self.bridge.Register(pb.PluginInfo( - id=PLUGIN_ID, - name="AI Test Plugin", - version="0.1.0", - plugin_type="basic", - syscalls=[ - "display.text", - "memory.write", "memory.read", "memory.list", "memory.delete", - "fs.write", "fs.read", "fs.stat", "fs.list", "fs.rename", "fs.delete", - "process.spawn", "process.stdout", "process.wait", - "timer.once", - ], - depends=[], - exports=[pb.ExportDef(name="run_checks", description="Run Agentsd verification checks")], - )) - if not resp.ok: - raise RuntimeError(resp.error) - return f"session={resp.session_id}" - - def display_text(self): - resp = self.display.Text( - pb.DisplayTextRequest(content=f"[ai-test] display check run={self.run_id}"), - metadata=METADATA, - ) - if not resp.ok: - raise RuntimeError(resp.error) - return "display.text ok" - - def memory_roundtrip(self): - key = f"ai_test.{self.run_id}.greeting" - value = f"hello-{self.run_id}".encode("utf-8") - resp = self.memory.Write(pb.MemoryWriteRequest(key=key, value=value), metadata=METADATA) - if not resp.ok: - raise RuntimeError(f"write: {resp.error}") - resp = self.memory.Read(pb.MemoryReadRequest(key=key), metadata=METADATA) - if not resp.found or resp.value != value: - raise RuntimeError(f"read mismatch found={resp.found} value={resp.value!r}") - resp = self.memory.List(pb.MemoryListRequest(prefix=f"ai_test.{self.run_id}", limit=10), metadata=METADATA) - if key not in resp.keys: - raise RuntimeError(f"list missing key; keys={list(resp.keys)} error={resp.error!r}") - resp = self.memory.Delete(pb.MemoryKeyRequest(key=key), metadata=METADATA) - if not resp.ok: - raise RuntimeError(f"delete: {resp.error}") - return f"key={key}" - - def fs_roundtrip(self): - base = self.scratch_dir - file_a = base / "sample.txt" - file_b = base / "renamed.txt" - data = f"AI_TEST_FS_OK {self.run_id}".encode("utf-8") - - resp = self.fs.Write(pb.FsWriteRequest(path=str(file_a), data=data, append=False), metadata=METADATA) - if not resp.ok: - raise RuntimeError(f"write: {resp.error}") - - resp = self.fs.Read(pb.FsReadRequest(path=str(file_a), offset=0, length=0), metadata=METADATA) - if not resp.ok or resp.data != data: - raise RuntimeError(f"read mismatch ok={resp.ok} data={resp.data!r} error={resp.error!r}") - - resp = self.fs.Stat(pb.FsPathRequest(path=str(file_a)), metadata=METADATA) - if not resp.ok or not resp.is_file or resp.size != len(data): - raise RuntimeError(f"stat mismatch ok={resp.ok} size={resp.size} error={resp.error!r}") - - resp = self.fs.List(pb.FsPathRequest(path=str(base)), metadata=METADATA) - names = [e.name for e in resp.entries] - if not resp.ok or "sample.txt" not in names: - raise RuntimeError(f"list mismatch ok={resp.ok} names={names} error={resp.error!r}") - - resp = self.fs.Rename(pb.FsRenameRequest(**{"from": str(file_a), "to": str(file_b)}), metadata=METADATA) - if not resp.ok: - raise RuntimeError(f"rename: {resp.error}") - - resp = self.fs.Delete(pb.FsPathRequest(path=str(base)), metadata=METADATA) - if not resp.ok: - raise RuntimeError(f"delete: {resp.error}") - return str(base) - - def process_roundtrip(self): - code = "print('AI_TEST_PROCESS_OK')" - resp = self.process.Spawn(pb.SpawnRequest( - cmd=sys.executable, - args=["-c", code], - env={}, - cwd=str(ROOT), - ), metadata=METADATA) - if not resp.ok: - raise RuntimeError(resp.error) - pid = resp.pid - - chunks = [] - for chunk in self.process.Stdout(pb.PidRequest(pid=pid), metadata=METADATA): - chunks.append(chunk.data) - output = b"".join(chunks).decode("utf-8", errors="replace").strip() - - wait = self.process.Wait(pb.PidRequest(pid=pid), metadata=METADATA) - if not wait.ok or wait.exit_code != 0: - raise RuntimeError(f"wait failed ok={wait.ok} code={wait.exit_code} error={wait.error!r}") - if output != "AI_TEST_PROCESS_OK": - raise RuntimeError(f"stdout mismatch: {output!r}") - return f"pid={pid} stdout={output}" - - def timer_event(self): - stream = self.bridge.StreamCall( - pb.CallRequest(target=PLUGIN_ID, fn="events", args=b""), - metadata=METADATA, - ) - resp = self.timer.Once(pb.TimerOnceRequest(delay_ms=100, callback_id="ai-test-callback"), metadata=METADATA) - if not resp.ok: - raise RuntimeError(resp.error) - - with ThreadPoolExecutor(max_workers=1) as executor: - future = executor.submit(next, stream) - try: - event = future.result(timeout=5) - except TimeoutError: - stream.cancel() - raise RuntimeError("timed out waiting for timer event") - - payload = json.loads(event.result.decode("utf-8")) - if payload.get("event") != "timer" or payload.get("callback_id") != "ai-test-callback": - raise RuntimeError(f"unexpected payload: {payload}") - return json.dumps(payload, ensure_ascii=False) - - def permission_denial(self): - try: - self.network.Available(pb.Empty(), metadata=METADATA) - except grpc.RpcError as exc: - if exc.code() == grpc.StatusCode.PERMISSION_DENIED: - return exc.details() - raise RuntimeError(f"wrong error: {exc.code().name} {exc.details()}") - raise RuntimeError("network.available unexpectedly succeeded") - - def run(self): - print(f"AI Test Plugin connecting to {ADDR}") - print(f"Scratch dir: {self.scratch_dir}") - self.check("register", self.register) - self.check("display.text", self.display_text) - self.check("memory roundtrip", self.memory_roundtrip) - self.check("fs roundtrip", self.fs_roundtrip) - self.check("process roundtrip", self.process_roundtrip) - self.check("timer event", self.timer_event) - self.check("permission denial", self.permission_denial) - - ok = all(r["ok"] for r in self.results) - summary = {"plugin": PLUGIN_ID, "ok": ok, "results": self.results} - print("AI_TEST_SUMMARY " + json.dumps(summary, ensure_ascii=False)) - return 0 if ok else 1 - - -def main(): - runner = CheckRunner() - try: - return runner.run() - finally: - runner.close() - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/plugins/ai_test/plugin.yaml b/plugins/ai_test/plugin.yaml index bd6cd74..51ac4a3 100644 --- a/plugins/ai_test/plugin.yaml +++ b/plugins/ai_test/plugin.yaml @@ -28,4 +28,4 @@ exports: - name: run_checks description: Run end-to-end Agentsd syscall checks for AI verification -entry: python ai_test_plugin.py +entry: cargo run -p agentsd-plugin-ai-test diff --git a/plugins/claudecode/claudecode_plugin.py b/plugins/claudecode/claudecode_plugin.py deleted file mode 100644 index 78825b9..0000000 --- a/plugins/claudecode/claudecode_plugin.py +++ /dev/null @@ -1,312 +0,0 @@ -""" -Claude Code Plugin for Agentsd -============================== - -Bridges locally installed Claude Code CLI into agentsd as a standard plugin. -Enables agentsd to use Claude Code for code generation, editing, and reasoning. - -Architecture: - agentsd (Rust) ←gRPC→ claudecode_plugin.py → claude CLI (subprocess) - -Capabilities: - - chat: Send a prompt to Claude Code, get response - - code: Generate/edit code with full Claude Code capabilities - - session: Resume previous Claude Code sessions - - tools: Let Claude Code use its built-in tools (Bash, Edit, Read, etc.) - -Usage: - # Start agentsd first, then: - python claudecode_plugin.py -""" - -import json -import logging -import os -import subprocess -import sys -import time -from pathlib import Path -from concurrent import futures - -import grpc - -# Add SDK path -SDK_DIR = Path(__file__).resolve().parent.parent / "_sdk" -sys.path.insert(0, str(SDK_DIR)) - -import agentsd_pb2 as pb -import agentsd_pb2_grpc as rpc - -logging.basicConfig(level=logging.INFO, format="[claudecode] %(levelname)s %(message)s") -logger = logging.getLogger(__name__) - -# Find claude CLI -CLAUDE_BIN = os.environ.get("CLAUDE_BIN", None) - - -def find_claude(): - """Verify claude CLI is available and return the path.""" - global CLAUDE_BIN - - if CLAUDE_BIN is None: - # Auto-detect - candidates = [ - "claude", - "claude.cmd", - os.path.expandvars(r"%APPDATA%\npm\claude.cmd"), - os.path.expanduser("~/.local/bin/claude"), - "/usr/local/bin/claude", - ] - for c in candidates: - try: - result = subprocess.run( - [c, "--version"], - capture_output=True, text=True, timeout=10 - ) - if result.returncode == 0: - CLAUDE_BIN = c - break - except (FileNotFoundError, subprocess.TimeoutExpired, OSError): - continue - - if CLAUDE_BIN is None: - logger.error("Claude Code CLI not found") - logger.error("Install: npm install -g @anthropic-ai/claude-code") - return None - - try: - result = subprocess.run( - [CLAUDE_BIN, "--version"], - capture_output=True, text=True, timeout=10 - ) - version = result.stdout.strip() - logger.info("Found Claude Code: %s at %s", version, CLAUDE_BIN) - return version - except Exception as e: - logger.error("Claude Code CLI error: %s", e) - return None - - -class ClaudeCodePlugin: - """Bridges Claude Code CLI into agentsd.""" - - def __init__(self, agentsd_addr: str = "localhost:50051"): - self.addr = agentsd_addr - self.channel = None - self.bridge = None - self.version = None - self.default_cwd = os.getcwd() - - def connect(self): - """Connect to agentsd.""" - self.channel = grpc.insecure_channel(self.addr) - self.bridge = rpc.PluginBridgeStub(self.channel) - logger.info("Connected to agentsd at %s", self.addr) - - def register(self): - """Register with agentsd.""" - resp = self.bridge.Register(pb.PluginInfo( - id="claudecode", - name="Claude Code", - version=self.version or "unknown", - plugin_type="standard", - syscalls=["process.spawn", "display.text", "fs.read", "fs.write", "network.request"], - depends=["hermes"], - exports=[ - pb.ExportDef(name="chat", description="Send prompt to Claude Code, get text response"), - pb.ExportDef(name="code", description="Generate or edit code with Claude Code"), - pb.ExportDef(name="review", description="Review code with Claude Code"), - pb.ExportDef(name="ask", description="Ask Claude Code a question about the codebase"), - pb.ExportDef(name="run", description="Run Claude Code with full tool access"), - ], - )) - if resp.ok: - logger.info("Registered with agentsd: session=%s", resp.session_id) - else: - logger.error("Registration failed: %s", resp.error) - return resp.ok - - def call_claude(self, prompt: str, cwd: str = None, max_turns: int = None, - allowed_tools: list = None, system_prompt: str = None, - output_format: str = "text") -> dict: - """ - Call Claude Code CLI in print mode. - - Args: - prompt: The prompt to send - cwd: Working directory for Claude Code - max_turns: Max agentic turns (default: unlimited) - allowed_tools: List of tools to allow (e.g. ["Bash", "Read", "Edit"]) - system_prompt: Override system prompt - output_format: "text" or "json" or "stream-json" - - Returns: - dict with "result" or "error" - """ - cmd = [CLAUDE_BIN, "-p", "--output-format", output_format] - - if max_turns is not None: - cmd.extend(["--max-turns", str(max_turns)]) - - if allowed_tools: - cmd.extend(["--allowedTools"] + allowed_tools) - - if system_prompt: - cmd.extend(["--system-prompt", system_prompt]) - - work_dir = cwd or self.default_cwd - - logger.info("Calling claude: cwd=%s prompt=%s...", work_dir, prompt[:60]) - - try: - result = subprocess.run( - cmd, - input=prompt, - capture_output=True, - text=True, - cwd=work_dir, - timeout=300, # 5 min max - env={**os.environ, "CLAUDE_CODE_SIMPLE": "1"}, - ) - - if result.returncode == 0: - output = result.stdout.strip() - if output_format == "json": - try: - return json.loads(output) - except json.JSONDecodeError: - return {"result": output} - return {"result": output} - else: - error = result.stderr.strip() or f"exit code {result.returncode}" - return {"error": error} - - except subprocess.TimeoutExpired: - return {"error": "timeout (300s)"} - except Exception as e: - return {"error": str(e)} - - def handle_call(self, fn_name: str, args: dict) -> dict: - """Route a call to the appropriate handler.""" - prompt = args.get("prompt", args.get("text", "")) - cwd = args.get("cwd", self.default_cwd) - - if fn_name == "chat": - return self.call_claude( - prompt=prompt, - cwd=cwd, - max_turns=1, # Single turn, no tools - allowed_tools=[], - output_format="text", - ) - - elif fn_name == "code": - return self.call_claude( - prompt=prompt, - cwd=cwd, - allowed_tools=["Bash", "Read", "Edit", "Write", "Glob", "Grep"], - output_format="text", - ) - - elif fn_name == "review": - review_prompt = f"Review the following code or changes. Be concise and actionable:\n\n{prompt}" - return self.call_claude( - prompt=review_prompt, - cwd=cwd, - max_turns=1, - allowed_tools=["Read", "Glob", "Grep"], - output_format="text", - ) - - elif fn_name == "ask": - return self.call_claude( - prompt=prompt, - cwd=cwd, - max_turns=3, - allowed_tools=["Read", "Glob", "Grep"], - output_format="text", - ) - - elif fn_name == "run": - # Full access mode - return self.call_claude( - prompt=prompt, - cwd=cwd, - output_format="text", - ) - - else: - return {"error": f"unknown function: {fn_name}"} - - def run_server(self, port: int = 50053): - """Run callback gRPC server for agentsd.""" - server = grpc.server(futures.ThreadPoolExecutor(max_workers=4)) - rpc.add_PluginBridgeServicer_to_server(ClaudeCodeServicer(self), server) - server.add_insecure_port(f"[::]:{port}") - server.start() - logger.info("Claude Code plugin server listening on port %d", port) - return server - - -class ClaudeCodeServicer(rpc.PluginBridgeServicer): - """gRPC servicer for Claude Code plugin.""" - - def __init__(self, plugin: ClaudeCodePlugin): - self.plugin = plugin - - def Call(self, request, context): - fn_name = request.fn - try: - args = json.loads(request.args) if request.args else {} - except json.JSONDecodeError: - args = {} - - logger.info("Call: %s(%s)", fn_name, list(args.keys())) - result = self.plugin.handle_call(fn_name, args) - result_bytes = json.dumps(result, ensure_ascii=False, default=str).encode("utf-8") - - if "error" in result and not result.get("result"): - return pb.CallResponse(result=result_bytes, ok=False, error=result["error"]) - return pb.CallResponse(result=result_bytes, ok=True, error="") - - def Register(self, request, context): - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - return pb.RegisterResponse(ok=False, error="not a bridge") - - def Heartbeat(self, request, context): - return pb.HeartbeatResponse(ok=True) - - def StreamCall(self, request, context): - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - return pb.CallResponse(ok=False, error="not implemented") - - -def main(): - # Verify claude CLI - version = find_claude() - if not version: - sys.exit(1) - - plugin = ClaudeCodePlugin() - plugin.version = version - - # Connect and register - plugin.connect() - if not plugin.register(): - sys.exit(1) - - # Start callback server - server = plugin.run_server(port=50053) - - logger.info("Claude Code plugin running (v%s). Exports: chat, code, review, ask, run", version) - try: - while True: - time.sleep(1) - except KeyboardInterrupt: - logger.info("Shutting down...") - server.stop(grace=5) - plugin.channel.close() - - -if __name__ == "__main__": - main() diff --git a/plugins/claudecode/plugin.yaml b/plugins/claudecode/plugin.yaml index 7689c23..049c778 100644 --- a/plugins/claudecode/plugin.yaml +++ b/plugins/claudecode/plugin.yaml @@ -25,5 +25,5 @@ exports: - name: run description: Full Claude Code access (all tools, unlimited turns) -entry: python claudecode_plugin.py +entry: cargo run -p agentsd-plugin-claudecode port: 50053 diff --git a/plugins/claudecode/test_chat.py b/plugins/claudecode/test_chat.py deleted file mode 100644 index 7b6eecf..0000000 --- a/plugins/claudecode/test_chat.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Quick test: call Claude Code plugin's chat export.""" -import json -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent.parent / "_sdk")) - -import grpc -import agentsd_pb2 as pb -import agentsd_pb2_grpc as rpc - - -def main(): - # Connect to claudecode plugin's callback server - channel = grpc.insecure_channel("localhost:50053") - bridge = rpc.PluginBridgeStub(channel) - - # Call "chat" - args = json.dumps({"prompt": "What is 2+2? Reply with just the number."}).encode() - resp = bridge.Call(pb.CallRequest(target="claudecode", fn="chat", args=args)) - print(f"ok={resp.ok}") - if resp.result: - result = json.loads(resp.result) - print(f"result: {result.get('result', result.get('error', ''))}") - channel.close() - - -if __name__ == "__main__": - main() diff --git a/plugins/echo/echo_plugin.py b/plugins/echo/echo_plugin.py deleted file mode 100644 index 6e4eae8..0000000 --- a/plugins/echo/echo_plugin.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -Echo plugin for agentsd - verifies the gRPC syscall chain works. -Registers as a basic plugin, calls Display.Text and FS.Read. -""" -import grpc -import sys -import os - -# Add SDK path -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "_sdk")) - -from agentsd_pb2 import ( - DisplayTextRequest, FsReadRequest, FsPathRequest, - MemoryWriteRequest, MemoryReadRequest, - PluginInfo, ExportDef, Empty -) -from agentsd_pb2_grpc import ( - DisplayStub, FSStub, MemoryStub, PluginBridgeStub -) - - -PLUGIN_ID = "echo-test" -METADATA = (("x-plugin-id", PLUGIN_ID),) - - -def main(): - channel = grpc.insecure_channel('localhost:50051') - - # Register plugin - bridge = PluginBridgeStub(channel) - resp = bridge.Register(PluginInfo( - id=PLUGIN_ID, - name="Echo Test Plugin", - version="0.1.0", - plugin_type="basic", - syscalls=["display.text", "fs.list", "memory.read", "memory.write"], - depends=[], - exports=[ExportDef(name="echo", description="Echo test")] - )) - print(f"Register: ok={resp.ok} session={resp.session_id}") - - # Test Display - display = DisplayStub(channel) - resp = display.Text(DisplayTextRequest(content="Hello from echo plugin!"), metadata=METADATA) - print(f"Display.Text: ok={resp.ok}") - - # Test Memory write + read - memory = MemoryStub(channel) - resp = memory.Write(MemoryWriteRequest(key="test.greeting", value=b"Hello World"), metadata=METADATA) - print(f"Memory.Write: ok={resp.ok}") - - resp = memory.Read(MemoryReadRequest(key="test.greeting"), metadata=METADATA) - print(f"Memory.Read: found={resp.found} value={resp.value.decode()}") - - # Test FS list - fs = FSStub(channel) - resp = fs.List(FsPathRequest(path="."), metadata=METADATA) - print(f"FS.List: ok={resp.ok} entries={len(resp.entries)}") - for entry in resp.entries[:5]: - print(f" {entry.name} {'[dir]' if entry.is_dir else ''}") - - print("\nAll syscalls verified OK!") - channel.close() - - -if __name__ == "__main__": - main() diff --git a/plugins/echo/plugin.yaml b/plugins/echo/plugin.yaml index b1379ed..56fe9f2 100644 --- a/plugins/echo/plugin.yaml +++ b/plugins/echo/plugin.yaml @@ -13,4 +13,4 @@ exports: - name: echo description: Echo test -entry: python echo_plugin.py +entry: cargo run -p agentsd-plugin-echo diff --git a/plugins/hermes/hermes_plugin.py b/plugins/hermes/hermes_plugin.py index 4538033..1e6aebf 100644 --- a/plugins/hermes/hermes_plugin.py +++ b/plugins/hermes/hermes_plugin.py @@ -45,8 +45,10 @@ logger = logging.getLogger(__name__) class HermesPlugin: """Bridges Hermes tools into agentsd.""" - def __init__(self, agentsd_addr: str = "localhost:50051"): - self.addr = agentsd_addr + def __init__(self, agentsd_addr: str = None): + # agentsd binds [::1]:50051 (IPv6 loopback); match it so "localhost" + # resolving to IPv4 first doesn't break the connection. AGENTSD_ADDR overrides. + self.addr = agentsd_addr or os.environ.get("AGENTSD_ADDR", "[::1]:50051") self.channel = None self.bridge = None self.registry = None diff --git a/proto/agentsd.proto b/proto/agentsd.proto index 9993760..0835818 100644 --- a/proto/agentsd.proto +++ b/proto/agentsd.proto @@ -93,7 +93,7 @@ message MemoryResponse { bool ok = 1; string error = 2; } // ========== Network ========== service Network { rpc Request(HttpRequest) returns (HttpResponse); - rpc OpenConn(ConnectRequest) returns (stream DataChunk); + rpc OpenConn(ConnectRequest) returns (stream ConnEvent); rpc Listen(ListenRequest) returns (stream ConnEvent); rpc Send(SendRequest) returns (NetResponse); rpc Close(CloseRequest) returns (NetResponse);