Compare commits
3 Commits
eaa99a20db
...
04f70c797d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04f70c797d | ||
|
|
a685e97b17 | ||
|
|
2037809196 |
38
Cargo.lock
generated
38
Cargo.lock
generated
@@ -5492,6 +5492,7 @@ version = "0.23.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -7858,6 +7859,35 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"flate2",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"ureq-proto",
|
||||
"utf8-zero",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ureq-proto"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"http 1.4.0",
|
||||
"httparse",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
@@ -7895,6 +7925,12 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf8-zero"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
@@ -9723,7 +9759,6 @@ dependencies = [
|
||||
"tracing",
|
||||
"uuid",
|
||||
"zclaw-hands",
|
||||
"zclaw-kernel",
|
||||
"zclaw-runtime",
|
||||
"zclaw-skills",
|
||||
"zclaw-types",
|
||||
@@ -9840,6 +9875,7 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"ureq",
|
||||
"uuid",
|
||||
"wasmtime",
|
||||
"wasmtime-wasi",
|
||||
|
||||
@@ -63,6 +63,9 @@ libsqlite3-sys = { version = "0.27", features = ["bundled"] }
|
||||
# HTTP client (for LLM drivers)
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls"] }
|
||||
|
||||
# Synchronous HTTP (for WASM host functions in blocking threads)
|
||||
ureq = { version = "3", features = ["rustls"] }
|
||||
|
||||
# URL parsing
|
||||
url = "2"
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ rust-version.workspace = true
|
||||
description = "ZCLAW kernel - central coordinator for all subsystems"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["multi-agent"]
|
||||
# Enable multi-agent orchestration (Director, A2A protocol)
|
||||
multi-agent = ["zclaw-protocols/a2a"]
|
||||
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
//! A2A (Agent-to-Agent) messaging
|
||||
//!
|
||||
//! All items in this module are gated by the `multi-agent` feature flag.
|
||||
|
||||
#[cfg(feature = "multi-agent")]
|
||||
use zclaw_types::{AgentId, Capability, Event, Result};
|
||||
#[cfg(feature = "multi-agent")]
|
||||
use zclaw_protocols::{A2aAgentProfile, A2aCapability, A2aEnvelope, A2aMessageType, A2aRecipient};
|
||||
|
||||
#[cfg(feature = "multi-agent")]
|
||||
use super::Kernel;
|
||||
|
||||
#[cfg(feature = "multi-agent")]
|
||||
impl Kernel {
|
||||
// ============================================================
|
||||
// A2A (Agent-to-Agent) Messaging
|
||||
|
||||
@@ -106,13 +106,11 @@ impl SkillExecutor for KernelSkillExecutor {
|
||||
|
||||
/// Inbox wrapper for A2A message receivers that supports re-queuing
|
||||
/// non-matching messages instead of dropping them.
|
||||
#[cfg(feature = "multi-agent")]
|
||||
pub(crate) struct AgentInbox {
|
||||
pub(crate) rx: tokio::sync::mpsc::Receiver<zclaw_protocols::A2aEnvelope>,
|
||||
pub(crate) pending: std::collections::VecDeque<zclaw_protocols::A2aEnvelope>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "multi-agent")]
|
||||
impl AgentInbox {
|
||||
pub(crate) fn new(rx: tokio::sync::mpsc::Receiver<zclaw_protocols::A2aEnvelope>) -> Self {
|
||||
Self { rx, pending: std::collections::VecDeque::new() }
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
use zclaw_types::{AgentConfig, AgentId, AgentInfo, Event, Result};
|
||||
|
||||
#[cfg(feature = "multi-agent")]
|
||||
use std::sync::Arc;
|
||||
#[cfg(feature = "multi-agent")]
|
||||
use tokio::sync::Mutex;
|
||||
#[cfg(feature = "multi-agent")]
|
||||
use super::adapters::AgentInbox;
|
||||
|
||||
use super::Kernel;
|
||||
@@ -23,7 +20,6 @@ impl Kernel {
|
||||
self.memory.save_agent(&config).await?;
|
||||
|
||||
// Register with A2A router for multi-agent messaging (before config is moved)
|
||||
#[cfg(feature = "multi-agent")]
|
||||
{
|
||||
let profile = Self::agent_config_to_a2a_profile(&config);
|
||||
let rx = self.a2a_router.register_agent(profile).await;
|
||||
@@ -52,7 +48,6 @@ impl Kernel {
|
||||
self.memory.delete_agent(id).await?;
|
||||
|
||||
// Unregister from A2A router
|
||||
#[cfg(feature = "multi-agent")]
|
||||
{
|
||||
self.a2a_router.unregister_agent(id).await;
|
||||
self.a2a_inboxes.remove(id);
|
||||
|
||||
@@ -8,16 +8,13 @@ mod hands;
|
||||
mod triggers;
|
||||
mod approvals;
|
||||
mod orchestration;
|
||||
#[cfg(feature = "multi-agent")]
|
||||
mod a2a;
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{broadcast, Mutex};
|
||||
use zclaw_types::{Event, Result, AgentState};
|
||||
|
||||
#[cfg(feature = "multi-agent")]
|
||||
use zclaw_types::AgentId;
|
||||
#[cfg(feature = "multi-agent")]
|
||||
use zclaw_protocols::A2aRouter;
|
||||
|
||||
use crate::registry::AgentRegistry;
|
||||
@@ -56,11 +53,9 @@ pub struct Kernel {
|
||||
mcp_adapters: Arc<std::sync::RwLock<Vec<zclaw_protocols::McpToolAdapter>>>,
|
||||
/// Dynamic industry keyword configs — shared with Tauri frontend, loaded from SaaS
|
||||
industry_keywords: Arc<tokio::sync::RwLock<Vec<zclaw_runtime::IndustryKeywordConfig>>>,
|
||||
/// A2A router for inter-agent messaging (gated by multi-agent feature)
|
||||
#[cfg(feature = "multi-agent")]
|
||||
/// A2A router for inter-agent messaging
|
||||
a2a_router: Arc<A2aRouter>,
|
||||
/// Per-agent A2A inbox receivers (supports re-queuing non-matching messages)
|
||||
#[cfg(feature = "multi-agent")]
|
||||
a2a_inboxes: Arc<dashmap::DashMap<AgentId, Arc<Mutex<adapters::AgentInbox>>>>,
|
||||
}
|
||||
|
||||
@@ -135,7 +130,6 @@ impl Kernel {
|
||||
}
|
||||
|
||||
// Initialize A2A router for multi-agent support
|
||||
#[cfg(feature = "multi-agent")]
|
||||
let a2a_router = {
|
||||
let kernel_agent_id = AgentId::new();
|
||||
Arc::new(A2aRouter::new(kernel_agent_id))
|
||||
@@ -159,9 +153,7 @@ impl Kernel {
|
||||
extraction_driver: None,
|
||||
mcp_adapters: Arc::new(std::sync::RwLock::new(Vec::new())),
|
||||
industry_keywords: Arc::new(tokio::sync::RwLock::new(Vec::new())),
|
||||
#[cfg(feature = "multi-agent")]
|
||||
a2a_router,
|
||||
#[cfg(feature = "multi-agent")]
|
||||
a2a_inboxes: Arc::new(dashmap::DashMap::new()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ pub mod trigger_manager;
|
||||
pub mod config;
|
||||
pub mod scheduler;
|
||||
pub mod skill_router;
|
||||
#[cfg(feature = "multi-agent")]
|
||||
pub mod director;
|
||||
pub mod generation;
|
||||
pub mod export;
|
||||
@@ -21,13 +20,11 @@ pub use capabilities::*;
|
||||
pub use events::*;
|
||||
pub use config::*;
|
||||
pub use trigger_manager::{TriggerManager, TriggerEntry, TriggerUpdateRequest, TriggerManagerConfig};
|
||||
#[cfg(feature = "multi-agent")]
|
||||
pub use director::{
|
||||
Director, DirectorConfig, DirectorBuilder, DirectorAgent,
|
||||
ConversationState, ScheduleStrategy,
|
||||
// Note: AgentRole is intentionally NOT re-exported here — use generation::AgentRole instead
|
||||
};
|
||||
#[cfg(feature = "multi-agent")]
|
||||
pub use zclaw_protocols::{
|
||||
A2aRouter, A2aAgentProfile, A2aCapability, A2aEnvelope, A2aMessageType, A2aRecipient,
|
||||
A2aReceiver,
|
||||
|
||||
@@ -9,7 +9,7 @@ description = "ZCLAW skill system"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
wasm = ["wasmtime", "wasmtime-wasi/p1"]
|
||||
wasm = ["wasmtime", "wasmtime-wasi/p1", "ureq"]
|
||||
|
||||
[dependencies]
|
||||
zclaw-types = { workspace = true }
|
||||
@@ -27,3 +27,4 @@ shlex = { workspace = true }
|
||||
# Optional WASM runtime (enable with --features wasm)
|
||||
wasmtime = { workspace = true, optional = true }
|
||||
wasmtime-wasi = { workspace = true, optional = true }
|
||||
ureq = { workspace = true, optional = true }
|
||||
|
||||
@@ -230,49 +230,100 @@ fn create_engine_config() -> Config {
|
||||
}
|
||||
|
||||
/// Add ZCLAW host functions to the wasmtime linker.
|
||||
fn add_host_functions(linker: &mut Linker<WasiP1Ctx>, _network_allowed: bool) -> Result<()> {
|
||||
fn add_host_functions(linker: &mut Linker<WasiP1Ctx>, network_allowed: bool) -> Result<()> {
|
||||
linker
|
||||
.func_wrap(
|
||||
"env",
|
||||
"zclaw_log",
|
||||
|_caller: Caller<'_, WasiP1Ctx>, _ptr: u32, _len: u32| {
|
||||
debug!("[WasmSkill] guest called zclaw_log");
|
||||
|mut caller: Caller<'_, WasiP1Ctx>, ptr: u32, len: u32| {
|
||||
let msg = read_guest_string(&mut caller, ptr, len);
|
||||
debug!("[WasmSkill] guest log: {}", msg);
|
||||
},
|
||||
)
|
||||
.map_err(|e| {
|
||||
zclaw_types::ZclawError::ToolError(format!("Failed to add zclaw_log: {}", e))
|
||||
})?;
|
||||
|
||||
// zclaw_http_fetch(url_ptr, url_len, out_ptr, out_cap) -> bytes_written (-1 = error)
|
||||
// Performs a synchronous GET request. Result is written to guest memory as JSON string.
|
||||
let net = network_allowed;
|
||||
linker
|
||||
.func_wrap(
|
||||
"env",
|
||||
"zclaw_http_fetch",
|
||||
|_caller: Caller<'_, WasiP1Ctx>,
|
||||
_url_ptr: u32,
|
||||
_url_len: u32,
|
||||
_out_ptr: u32,
|
||||
_out_cap: u32|
|
||||
-> i32 {
|
||||
warn!("[WasmSkill] guest called zclaw_http_fetch — denied");
|
||||
-1
|
||||
move |mut caller: Caller<'_, WasiP1Ctx>,
|
||||
url_ptr: u32,
|
||||
url_len: u32,
|
||||
out_ptr: u32,
|
||||
out_cap: u32|
|
||||
-> i32 {
|
||||
if !net {
|
||||
warn!("[WasmSkill] guest called zclaw_http_fetch — denied (network not allowed)");
|
||||
return -1;
|
||||
}
|
||||
|
||||
let url = read_guest_string(&mut caller, url_ptr, url_len);
|
||||
if url.is_empty() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
debug!("[WasmSkill] guest http_fetch: {}", url);
|
||||
|
||||
// Synchronous HTTP GET (we're already on a blocking thread)
|
||||
let agent = ureq::Agent::config_builder()
|
||||
.timeout_global(Some(std::time::Duration::from_secs(10)))
|
||||
.build()
|
||||
.new_agent();
|
||||
let response = agent.get(&url).call();
|
||||
|
||||
match response {
|
||||
Ok(mut resp) => {
|
||||
let body = resp.body_mut().read_to_string().unwrap_or_default();
|
||||
write_guest_bytes(&mut caller, out_ptr, out_cap, body.as_bytes())
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("[WasmSkill] http_fetch error for {}: {}", url, e);
|
||||
-1
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.map_err(|e| {
|
||||
zclaw_types::ZclawError::ToolError(format!("Failed to add zclaw_http_fetch: {}", e))
|
||||
})?;
|
||||
|
||||
// zclaw_file_read(path_ptr, path_len, out_ptr, out_cap) -> bytes_written (-1 = error)
|
||||
// Reads a file from the preopened /workspace directory. Paths must be relative.
|
||||
linker
|
||||
.func_wrap(
|
||||
"env",
|
||||
"zclaw_file_read",
|
||||
|_caller: Caller<'_, WasiP1Ctx>,
|
||||
_path_ptr: u32,
|
||||
_path_len: u32,
|
||||
_out_ptr: u32,
|
||||
_out_cap: u32|
|
||||
|mut caller: Caller<'_, WasiP1Ctx>,
|
||||
path_ptr: u32,
|
||||
path_len: u32,
|
||||
out_ptr: u32,
|
||||
out_cap: u32|
|
||||
-> i32 {
|
||||
warn!("[WasmSkill] guest called zclaw_file_read — denied");
|
||||
-1
|
||||
let path = read_guest_string(&mut caller, path_ptr, path_len);
|
||||
if path.is_empty() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Security: only allow reads under /workspace (preopen root)
|
||||
if path.starts_with("..") || path.starts_with('/') {
|
||||
warn!("[WasmSkill] guest file_read denied — path escapes sandbox: {}", path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
let full_path = format!("/workspace/{}", path);
|
||||
|
||||
match std::fs::read(&full_path) {
|
||||
Ok(data) => write_guest_bytes(&mut caller, out_ptr, out_cap, &data),
|
||||
Err(e) => {
|
||||
debug!("[WasmSkill] file_read error for {}: {}", path, e);
|
||||
-1
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.map_err(|e| {
|
||||
@@ -282,6 +333,38 @@ fn add_host_functions(linker: &mut Linker<WasiP1Ctx>, _network_allowed: bool) ->
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read a string from WASM guest memory.
|
||||
fn read_guest_string(caller: &mut Caller<'_, WasiP1Ctx>, ptr: u32, len: u32) -> String {
|
||||
let mem = match caller.get_export("memory") {
|
||||
Some(Extern::Memory(m)) => m,
|
||||
_ => return String::new(),
|
||||
};
|
||||
let offset = ptr as usize;
|
||||
let length = len as usize;
|
||||
let data = mem.data(&caller);
|
||||
if offset + length > data.len() {
|
||||
return String::new();
|
||||
}
|
||||
String::from_utf8_lossy(&data[offset..offset + length]).into_owned()
|
||||
}
|
||||
|
||||
/// Write bytes to WASM guest memory. Returns the number of bytes written, or -1 on overflow.
|
||||
fn write_guest_bytes(caller: &mut Caller<'_, WasiP1Ctx>, ptr: u32, cap: u32, data: &[u8]) -> i32 {
|
||||
let mem = match caller.get_export("memory") {
|
||||
Some(Extern::Memory(m)) => m,
|
||||
_ => return -1,
|
||||
};
|
||||
let offset = ptr as usize;
|
||||
let capacity = cap as usize;
|
||||
let write_len = data.len().min(capacity);
|
||||
if offset + write_len > mem.data_size(&caller) {
|
||||
return -1;
|
||||
}
|
||||
// Safety: we've bounds-checked the write region.
|
||||
mem.data_mut(&mut *caller)[offset..offset + write_len].copy_from_slice(&data[..write_len]);
|
||||
write_len as i32
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! A2A (Agent-to-Agent) commands — gated behind `multi-agent` feature
|
||||
//! A2A (Agent-to-Agent) commands
|
||||
|
||||
use serde_json;
|
||||
use tauri::State;
|
||||
@@ -7,10 +7,9 @@ use zclaw_types::AgentId;
|
||||
use super::KernelState;
|
||||
|
||||
// ============================================================
|
||||
// A2A (Agent-to-Agent) Commands — gated behind multi-agent feature
|
||||
// A2A (Agent-to-Agent) Commands
|
||||
// ============================================================
|
||||
|
||||
#[cfg(feature = "multi-agent")]
|
||||
/// Send a direct A2A message from one agent to another
|
||||
// @connected
|
||||
#[tauri::command]
|
||||
@@ -44,7 +43,6 @@ pub async fn agent_a2a_send(
|
||||
}
|
||||
|
||||
/// Broadcast a message from one agent to all other agents
|
||||
#[cfg(feature = "multi-agent")]
|
||||
// @connected
|
||||
#[tauri::command]
|
||||
pub async fn agent_a2a_broadcast(
|
||||
@@ -66,7 +64,6 @@ pub async fn agent_a2a_broadcast(
|
||||
}
|
||||
|
||||
/// Discover agents with a specific capability
|
||||
#[cfg(feature = "multi-agent")]
|
||||
// @connected
|
||||
#[tauri::command]
|
||||
pub async fn agent_a2a_discover(
|
||||
@@ -88,7 +85,6 @@ pub async fn agent_a2a_discover(
|
||||
}
|
||||
|
||||
/// Delegate a task to another agent and wait for response
|
||||
#[cfg(feature = "multi-agent")]
|
||||
// @connected
|
||||
#[tauri::command]
|
||||
pub async fn agent_a2a_delegate_task(
|
||||
@@ -116,11 +112,10 @@ pub async fn agent_a2a_delegate_task(
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Butler Delegation Command — multi-agent feature
|
||||
// Butler Delegation Command
|
||||
// ============================================================
|
||||
|
||||
/// Butler delegates a user request to expert agents via the Director.
|
||||
#[cfg(feature = "multi-agent")]
|
||||
// @reserved: butler multi-agent delegation
|
||||
// @connected
|
||||
#[tauri::command]
|
||||
|
||||
@@ -19,7 +19,6 @@ pub mod skill;
|
||||
pub mod trigger;
|
||||
pub mod workspace;
|
||||
|
||||
#[cfg(feature = "multi-agent")]
|
||||
pub mod a2a;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -255,16 +255,11 @@ pub fn run() {
|
||||
kernel_commands::scheduled_task::scheduled_task_create,
|
||||
kernel_commands::scheduled_task::scheduled_task_list,
|
||||
|
||||
// A2A commands gated behind multi-agent feature
|
||||
#[cfg(feature = "multi-agent")]
|
||||
// A2A commands
|
||||
kernel_commands::a2a::agent_a2a_send,
|
||||
#[cfg(feature = "multi-agent")]
|
||||
kernel_commands::a2a::agent_a2a_broadcast,
|
||||
#[cfg(feature = "multi-agent")]
|
||||
kernel_commands::a2a::agent_a2a_discover,
|
||||
#[cfg(feature = "multi-agent")]
|
||||
kernel_commands::a2a::agent_a2a_delegate_task,
|
||||
#[cfg(feature = "multi-agent")]
|
||||
kernel_commands::a2a::butler_delegate_task,
|
||||
|
||||
// Pipeline commands (DSL-based workflows)
|
||||
|
||||
14
wiki/log.md
14
wiki/log.md
@@ -9,6 +9,20 @@ tags: [log, history]
|
||||
|
||||
> Append-only 操作记录。格式: `## [日期] 类型 | 描述`
|
||||
|
||||
## 2026-04-17 refactor | Phase 4A multi-agent feature gate 移除
|
||||
|
||||
- 8 个文件移除 33 处 `#[cfg(feature = "multi-agent")]`
|
||||
- zclaw-kernel default features 新增 multi-agent,始终编译
|
||||
- A2A router、agents、adapters 代码不再条件编译
|
||||
|
||||
## 2026-04-17 feat | Phase 4B WASM host 函数真实实现
|
||||
|
||||
- zclaw_log: 读取 guest 内存字符串 + debug! 日志
|
||||
- zclaw_http_fetch: ureq v3 同步 GET (10s timeout, network_allowed 守卫)
|
||||
- zclaw_file_read: 沙箱 /workspace 读取 + 路径校验防逃逸
|
||||
- 新增 ureq v3 workspace 依赖 (wasm feature gated)
|
||||
- 25 测试全通过,workspace check 零错误
|
||||
|
||||
## 2026-04-17 refactor | Phase 3A loop_runner 双路径合并
|
||||
|
||||
- middleware_chain 从 Option<MiddlewareChain> 改为 MiddlewareChain (Default = 空链)
|
||||
|
||||
Reference in New Issue
Block a user