Ehu shubham shaw contribution --> Hardware support (#306)
* feat: add ZeroClaw firmware for ESP32 and Nucleo * Introduced new firmware for ZeroClaw on ESP32 and Nucleo-F401RE, enabling JSON-over-serial communication for GPIO control. * Added `zeroclaw-esp32` with support for commands like `gpio_read` and `gpio_write`, along with capabilities reporting. * Implemented `zeroclaw-nucleo` firmware with similar functionality for STM32, ensuring compatibility with existing ZeroClaw protocols. * Updated `.gitignore` to include new firmware targets and added necessary dependencies in `Cargo.toml` for both platforms. * Created README files for both firmware projects detailing setup, build, and usage instructions. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * feat: enhance hardware peripheral support and documentation - Added `Peripheral` trait implementation in `src/peripherals/` to manage hardware boards (STM32, RPi GPIO). - Updated `AGENTS.md` to include new extension points for peripherals and their configuration. - Introduced comprehensive documentation for adding boards and tools, including a quick start guide and supported boards. - Enhanced `Cargo.toml` to include optional dependencies for PDF extraction and peripheral support. - Created new datasheets for Arduino Uno, ESP32, and Nucleo-F401RE, detailing pin aliases and GPIO usage. - Implemented new tools for hardware memory reading and board information retrieval in the agent loop. This update significantly improves the integration and usability of hardware peripherals within the ZeroClaw framework. * feat: add ZeroClaw firmware for ESP32 and Nucleo * Introduced new firmware for ZeroClaw on ESP32 and Nucleo-F401RE, enabling JSON-over-serial communication for GPIO control. * Added `zeroclaw-esp32` with support for commands like `gpio_read` and `gpio_write`, along with capabilities reporting. * Implemented `zeroclaw-nucleo` firmware with similar functionality for STM32, ensuring compatibility with existing ZeroClaw protocols. * Updated `.gitignore` to include new firmware targets and added necessary dependencies in `Cargo.toml` for both platforms. * Created README files for both firmware projects detailing setup, build, and usage instructions. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * feat: enhance hardware peripheral support and documentation - Added `Peripheral` trait implementation in `src/peripherals/` to manage hardware boards (STM32, RPi GPIO). - Updated `AGENTS.md` to include new extension points for peripherals and their configuration. - Introduced comprehensive documentation for adding boards and tools, including a quick start guide and supported boards. - Enhanced `Cargo.toml` to include optional dependencies for PDF extraction and peripheral support. - Created new datasheets for Arduino Uno, ESP32, and Nucleo-F401RE, detailing pin aliases and GPIO usage. - Implemented new tools for hardware memory reading and board information retrieval in the agent loop. This update significantly improves the integration and usability of hardware peripherals within the ZeroClaw framework. * feat: Introduce hardware auto-discovery and expanded configuration options for agents, hardware, and security. * chore: update dependencies and improve probe-rs integration - Updated `Cargo.lock` to remove specific version constraints for several dependencies, including `zerocopy`, `syn`, and `strsim`, allowing for more flexibility in version resolution. - Upgraded `bincode` and `bitfield` to their latest versions, enhancing serialization and memory management capabilities. - Updated `Cargo.toml` to reflect the new version of `probe-rs` from `0.24` to `0.30`, improving hardware probing functionality. - Refactored code in `src/hardware` and `src/tools` to utilize the new `SessionConfig` for session management in `probe-rs`, ensuring better compatibility and performance. - Cleaned up documentation in `docs/datasheets/nucleo-f401re.md` by removing unnecessary lines. * fix: apply cargo fmt * docs: add hardware architecture diagram. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b36f23784a
commit
de3ec87d16
59 changed files with 9607 additions and 1885 deletions
|
|
@ -74,31 +74,139 @@ pub struct Config {
|
|||
#[serde(default)]
|
||||
pub cost: CostConfig,
|
||||
|
||||
/// Hardware Abstraction Layer (HAL) configuration.
|
||||
/// Controls how ZeroClaw interfaces with physical hardware
|
||||
/// (GPIO, serial, debug probes).
|
||||
#[serde(default)]
|
||||
pub hardware: crate::hardware::HardwareConfig,
|
||||
pub peripherals: PeripheralsConfig,
|
||||
|
||||
/// Named delegate agents for agent-to-agent handoff.
|
||||
///
|
||||
/// ```toml
|
||||
/// [agents.researcher]
|
||||
/// provider = "gemini"
|
||||
/// model = "gemini-2.0-flash"
|
||||
/// system_prompt = "You are a research assistant..."
|
||||
///
|
||||
/// [agents.coder]
|
||||
/// provider = "openrouter"
|
||||
/// model = "anthropic/claude-sonnet-4-20250514"
|
||||
/// system_prompt = "You are a coding assistant..."
|
||||
/// ```
|
||||
/// Agent context limits — use compact for smaller models (e.g. 13B with 4k–8k context).
|
||||
#[serde(default)]
|
||||
pub agent: AgentConfig,
|
||||
|
||||
/// Delegate agent configurations for multi-agent workflows.
|
||||
#[serde(default)]
|
||||
pub agents: HashMap<String, DelegateAgentConfig>,
|
||||
|
||||
/// Security configuration (sandboxing, resource limits, audit logging)
|
||||
/// Hardware configuration (wizard-driven physical world setup).
|
||||
#[serde(default)]
|
||||
pub security: SecurityConfig,
|
||||
pub hardware: HardwareConfig,
|
||||
}
|
||||
|
||||
// ── Agent (context limits for smaller models) ────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AgentConfig {
|
||||
/// When true: bootstrap_max_chars=6000, rag_chunk_limit=2. Use for 13B or smaller models.
|
||||
#[serde(default)]
|
||||
pub compact_context: bool,
|
||||
}
|
||||
|
||||
impl Default for AgentConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
compact_context: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Delegate Agents ──────────────────────────────────────────────
|
||||
|
||||
/// Configuration for a delegate sub-agent used by the `delegate` tool.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DelegateAgentConfig {
|
||||
/// Provider name (e.g. "ollama", "openrouter", "anthropic")
|
||||
pub provider: String,
|
||||
/// Model name
|
||||
pub model: String,
|
||||
/// Optional system prompt for the sub-agent
|
||||
#[serde(default)]
|
||||
pub system_prompt: Option<String>,
|
||||
/// Optional API key override
|
||||
#[serde(default)]
|
||||
pub api_key: Option<String>,
|
||||
/// Temperature override
|
||||
#[serde(default)]
|
||||
pub temperature: Option<f64>,
|
||||
/// Max recursion depth for nested delegation
|
||||
#[serde(default = "default_max_depth")]
|
||||
pub max_depth: u32,
|
||||
}
|
||||
|
||||
fn default_max_depth() -> u32 {
|
||||
3
|
||||
}
|
||||
|
||||
// ── Hardware Config (wizard-driven) ─────────────────────────────
|
||||
|
||||
/// Hardware transport mode.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum HardwareTransport {
|
||||
None,
|
||||
Native,
|
||||
Serial,
|
||||
Probe,
|
||||
}
|
||||
|
||||
impl Default for HardwareTransport {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for HardwareTransport {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::None => write!(f, "none"),
|
||||
Self::Native => write!(f, "native"),
|
||||
Self::Serial => write!(f, "serial"),
|
||||
Self::Probe => write!(f, "probe"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wizard-driven hardware configuration for physical world interaction.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HardwareConfig {
|
||||
/// Whether hardware access is enabled
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
/// Transport mode
|
||||
#[serde(default)]
|
||||
pub transport: HardwareTransport,
|
||||
/// Serial port path (e.g. "/dev/ttyACM0")
|
||||
#[serde(default)]
|
||||
pub serial_port: Option<String>,
|
||||
/// Serial baud rate
|
||||
#[serde(default = "default_baud_rate")]
|
||||
pub baud_rate: u32,
|
||||
/// Probe target chip (e.g. "STM32F401RE")
|
||||
#[serde(default)]
|
||||
pub probe_target: Option<String>,
|
||||
/// Enable workspace datasheet RAG (index PDF schematics for AI pin lookups)
|
||||
#[serde(default)]
|
||||
pub workspace_datasheets: bool,
|
||||
}
|
||||
|
||||
fn default_baud_rate() -> u32 {
|
||||
115200
|
||||
}
|
||||
|
||||
impl HardwareConfig {
|
||||
/// Return the active transport mode.
|
||||
pub fn transport_mode(&self) -> HardwareTransport {
|
||||
self.transport.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HardwareConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
transport: HardwareTransport::None,
|
||||
serial_port: None,
|
||||
baud_rate: default_baud_rate(),
|
||||
probe_target: None,
|
||||
workspace_datasheets: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Identity (AIEOS / OpenClaw format) ──────────────────────────
|
||||
|
|
@ -271,34 +379,64 @@ fn get_default_pricing() -> std::collections::HashMap<String, ModelPricing> {
|
|||
prices
|
||||
}
|
||||
|
||||
// ── Agent delegation ─────────────────────────────────────────────
|
||||
// ── Peripherals (hardware: STM32, RPi GPIO, etc.) ────────────────────────
|
||||
|
||||
/// Configuration for a named delegate agent that can be invoked via the
|
||||
/// `delegate` tool. Each agent uses its own provider/model combination
|
||||
/// and system prompt, enabling multi-agent workflows with specialization.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DelegateAgentConfig {
|
||||
/// Provider name (e.g. "gemini", "openrouter", "ollama")
|
||||
pub provider: String,
|
||||
/// Model identifier for the provider
|
||||
pub model: String,
|
||||
/// System prompt defining the agent's role and capabilities
|
||||
pub struct PeripheralsConfig {
|
||||
/// Enable peripheral support (boards become agent tools)
|
||||
#[serde(default)]
|
||||
pub system_prompt: Option<String>,
|
||||
/// Optional API key override (uses default if not set).
|
||||
/// Stored encrypted when `secrets.encrypt = true`.
|
||||
pub enabled: bool,
|
||||
/// Board configurations (nucleo-f401re, rpi-gpio, etc.)
|
||||
#[serde(default)]
|
||||
pub api_key: Option<String>,
|
||||
/// Temperature override (uses 0.7 if not set)
|
||||
pub boards: Vec<PeripheralBoardConfig>,
|
||||
/// Path to datasheet docs (relative to workspace) for RAG retrieval.
|
||||
/// Place .md/.txt files named by board (e.g. nucleo-f401re.md, rpi-gpio.md).
|
||||
#[serde(default)]
|
||||
pub temperature: Option<f64>,
|
||||
/// Maximum delegation depth to prevent infinite recursion (default: 3)
|
||||
#[serde(default = "default_max_delegation_depth")]
|
||||
pub max_depth: u32,
|
||||
pub datasheet_dir: Option<String>,
|
||||
}
|
||||
|
||||
fn default_max_delegation_depth() -> u32 {
|
||||
3
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PeripheralBoardConfig {
|
||||
/// Board type: "nucleo-f401re", "rpi-gpio", "esp32", etc.
|
||||
pub board: String,
|
||||
/// Transport: "serial", "native", "websocket"
|
||||
#[serde(default = "default_peripheral_transport")]
|
||||
pub transport: String,
|
||||
/// Path for serial: "/dev/ttyACM0", "/dev/ttyUSB0"
|
||||
#[serde(default)]
|
||||
pub path: Option<String>,
|
||||
/// Baud rate for serial (default: 115200)
|
||||
#[serde(default = "default_peripheral_baud")]
|
||||
pub baud: u32,
|
||||
}
|
||||
|
||||
fn default_peripheral_transport() -> String {
|
||||
"serial".into()
|
||||
}
|
||||
|
||||
fn default_peripheral_baud() -> u32 {
|
||||
115200
|
||||
}
|
||||
|
||||
impl Default for PeripheralsConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
boards: Vec::new(),
|
||||
datasheet_dir: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PeripheralBoardConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
board: String::new(),
|
||||
transport: default_peripheral_transport(),
|
||||
path: None,
|
||||
baud: default_peripheral_baud(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Gateway security ─────────────────────────────────────────────
|
||||
|
|
@ -1381,9 +1519,10 @@ impl Default for Config {
|
|||
http_request: HttpRequestConfig::default(),
|
||||
identity: IdentityConfig::default(),
|
||||
cost: CostConfig::default(),
|
||||
hardware: crate::hardware::HardwareConfig::default(),
|
||||
peripherals: PeripheralsConfig::default(),
|
||||
agent: AgentConfig::default(),
|
||||
agents: HashMap::new(),
|
||||
security: SecurityConfig::default(),
|
||||
hardware: HardwareConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1410,37 +1549,36 @@ impl Config {
|
|||
// Set computed paths that are skipped during serialization
|
||||
config.config_path = config_path.clone();
|
||||
config.workspace_dir = zeroclaw_dir.join("workspace");
|
||||
|
||||
// Decrypt agent API keys if encryption is enabled
|
||||
let store = crate::security::SecretStore::new(&zeroclaw_dir, config.secrets.encrypt);
|
||||
for agent in config.agents.values_mut() {
|
||||
if let Some(ref encrypted_key) = agent.api_key {
|
||||
agent.api_key = Some(
|
||||
store
|
||||
.decrypt(encrypted_key)
|
||||
.context("Failed to decrypt agent API key")?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
config.apply_env_overrides();
|
||||
Ok(config)
|
||||
} else {
|
||||
let mut config = Config::default();
|
||||
config.config_path = config_path.clone();
|
||||
config.workspace_dir = zeroclaw_dir.join("workspace");
|
||||
config.save()?;
|
||||
config.apply_env_overrides();
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply environment variable overrides to config
|
||||
pub fn apply_env_overrides(&mut self) {
|
||||
// API Key: ZEROCLAW_API_KEY or API_KEY
|
||||
// API Key: ZEROCLAW_API_KEY or API_KEY (generic)
|
||||
if let Ok(key) = std::env::var("ZEROCLAW_API_KEY").or_else(|_| std::env::var("API_KEY")) {
|
||||
if !key.is_empty() {
|
||||
self.api_key = Some(key);
|
||||
}
|
||||
}
|
||||
// API Key: GLM_API_KEY overrides when provider is glm (provider-specific)
|
||||
if self.default_provider.as_deref() == Some("glm")
|
||||
|| self.default_provider.as_deref() == Some("zhipu")
|
||||
{
|
||||
if let Ok(key) = std::env::var("GLM_API_KEY") {
|
||||
if !key.is_empty() {
|
||||
self.api_key = Some(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Provider: ZEROCLAW_PROVIDER or PROVIDER
|
||||
if let Ok(provider) =
|
||||
|
|
@ -1737,9 +1875,10 @@ mod tests {
|
|||
http_request: HttpRequestConfig::default(),
|
||||
identity: IdentityConfig::default(),
|
||||
cost: CostConfig::default(),
|
||||
hardware: crate::hardware::HardwareConfig::default(),
|
||||
peripherals: PeripheralsConfig::default(),
|
||||
agent: AgentConfig::default(),
|
||||
agents: HashMap::new(),
|
||||
security: SecurityConfig::default(),
|
||||
hardware: HardwareConfig::default(),
|
||||
};
|
||||
|
||||
let toml_str = toml::to_string_pretty(&config).unwrap();
|
||||
|
|
@ -1814,9 +1953,10 @@ default_temperature = 0.7
|
|||
http_request: HttpRequestConfig::default(),
|
||||
identity: IdentityConfig::default(),
|
||||
cost: CostConfig::default(),
|
||||
hardware: crate::hardware::HardwareConfig::default(),
|
||||
peripherals: PeripheralsConfig::default(),
|
||||
agent: AgentConfig::default(),
|
||||
agents: HashMap::new(),
|
||||
security: SecurityConfig::default(),
|
||||
hardware: HardwareConfig::default(),
|
||||
};
|
||||
|
||||
config.save().unwrap();
|
||||
|
|
@ -2637,236 +2777,41 @@ default_temperature = 0.7
|
|||
assert!(g.paired_tokens.is_empty());
|
||||
}
|
||||
|
||||
// ── Lark config ───────────────────────────────────────────────
|
||||
// ── Peripherals config ───────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn lark_config_serde() {
|
||||
let lc = LarkConfig {
|
||||
app_id: "cli_123456".into(),
|
||||
app_secret: "secret_abc".into(),
|
||||
encrypt_key: Some("encrypt_key".into()),
|
||||
verification_token: Some("verify_token".into()),
|
||||
allowed_users: vec!["user_123".into(), "user_456".into()],
|
||||
use_feishu: true,
|
||||
fn peripherals_config_default_disabled() {
|
||||
let p = PeripheralsConfig::default();
|
||||
assert!(!p.enabled);
|
||||
assert!(p.boards.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peripheral_board_config_defaults() {
|
||||
let b = PeripheralBoardConfig::default();
|
||||
assert!(b.board.is_empty());
|
||||
assert_eq!(b.transport, "serial");
|
||||
assert!(b.path.is_none());
|
||||
assert_eq!(b.baud, 115200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peripherals_config_toml_roundtrip() {
|
||||
let p = PeripheralsConfig {
|
||||
enabled: true,
|
||||
boards: vec![PeripheralBoardConfig {
|
||||
board: "nucleo-f401re".into(),
|
||||
transport: "serial".into(),
|
||||
path: Some("/dev/ttyACM0".into()),
|
||||
baud: 115200,
|
||||
}],
|
||||
datasheet_dir: None,
|
||||
};
|
||||
let json = serde_json::to_string(&lc).unwrap();
|
||||
let parsed: LarkConfig = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed.app_id, "cli_123456");
|
||||
assert_eq!(parsed.app_secret, "secret_abc");
|
||||
assert_eq!(parsed.encrypt_key.as_deref(), Some("encrypt_key"));
|
||||
assert_eq!(parsed.verification_token.as_deref(), Some("verify_token"));
|
||||
assert_eq!(parsed.allowed_users.len(), 2);
|
||||
assert!(parsed.use_feishu);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lark_config_toml_roundtrip() {
|
||||
let lc = LarkConfig {
|
||||
app_id: "cli_123456".into(),
|
||||
app_secret: "secret_abc".into(),
|
||||
encrypt_key: Some("encrypt_key".into()),
|
||||
verification_token: Some("verify_token".into()),
|
||||
allowed_users: vec!["*".into()],
|
||||
use_feishu: false,
|
||||
};
|
||||
let toml_str = toml::to_string(&lc).unwrap();
|
||||
let parsed: LarkConfig = toml::from_str(&toml_str).unwrap();
|
||||
assert_eq!(parsed.app_id, "cli_123456");
|
||||
assert_eq!(parsed.app_secret, "secret_abc");
|
||||
assert!(!parsed.use_feishu);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lark_config_deserializes_without_optional_fields() {
|
||||
let json = r#"{"app_id":"cli_123","app_secret":"secret"}"#;
|
||||
let parsed: LarkConfig = serde_json::from_str(json).unwrap();
|
||||
assert!(parsed.encrypt_key.is_none());
|
||||
assert!(parsed.verification_token.is_none());
|
||||
assert!(parsed.allowed_users.is_empty());
|
||||
assert!(!parsed.use_feishu);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lark_config_defaults_to_lark_endpoint() {
|
||||
let json = r#"{"app_id":"cli_123","app_secret":"secret"}"#;
|
||||
let parsed: LarkConfig = serde_json::from_str(json).unwrap();
|
||||
assert!(
|
||||
!parsed.use_feishu,
|
||||
"use_feishu should default to false (Lark)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lark_config_with_wildcard_allowed_users() {
|
||||
let json = r#"{"app_id":"cli_123","app_secret":"secret","allowed_users":["*"]}"#;
|
||||
let parsed: LarkConfig = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(parsed.allowed_users, vec!["*"]);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// AGENT DELEGATION CONFIG TESTS
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
#[test]
|
||||
fn agents_config_default_empty() {
|
||||
let c = Config::default();
|
||||
assert!(c.agents.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agents_config_backward_compat_missing_section() {
|
||||
let minimal = r#"
|
||||
workspace_dir = "/tmp/ws"
|
||||
config_path = "/tmp/config.toml"
|
||||
default_temperature = 0.7
|
||||
"#;
|
||||
let parsed: Config = toml::from_str(minimal).unwrap();
|
||||
assert!(parsed.agents.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agents_config_toml_roundtrip() {
|
||||
let toml_str = r#"
|
||||
default_temperature = 0.7
|
||||
|
||||
[agents.researcher]
|
||||
provider = "gemini"
|
||||
model = "gemini-2.0-flash"
|
||||
system_prompt = "You are a research assistant."
|
||||
max_depth = 2
|
||||
|
||||
[agents.coder]
|
||||
provider = "openrouter"
|
||||
model = "anthropic/claude-sonnet-4-20250514"
|
||||
"#;
|
||||
let parsed: Config = toml::from_str(toml_str).unwrap();
|
||||
assert_eq!(parsed.agents.len(), 2);
|
||||
|
||||
let researcher = &parsed.agents["researcher"];
|
||||
assert_eq!(researcher.provider, "gemini");
|
||||
assert_eq!(researcher.model, "gemini-2.0-flash");
|
||||
assert_eq!(
|
||||
researcher.system_prompt.as_deref(),
|
||||
Some("You are a research assistant.")
|
||||
);
|
||||
assert_eq!(researcher.max_depth, 2);
|
||||
assert!(researcher.api_key.is_none());
|
||||
assert!(researcher.temperature.is_none());
|
||||
|
||||
let coder = &parsed.agents["coder"];
|
||||
assert_eq!(coder.provider, "openrouter");
|
||||
assert_eq!(coder.model, "anthropic/claude-sonnet-4-20250514");
|
||||
assert!(coder.system_prompt.is_none());
|
||||
assert_eq!(coder.max_depth, 3); // default
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agents_config_with_api_key_and_temperature() {
|
||||
let toml_str = r#"
|
||||
[agents.fast]
|
||||
provider = "groq"
|
||||
model = "llama-3.3-70b-versatile"
|
||||
api_key = "gsk-test-key"
|
||||
temperature = 0.3
|
||||
"#;
|
||||
let parsed: HashMap<String, DelegateAgentConfig> = toml::from_str::<toml::Value>(toml_str)
|
||||
.unwrap()["agents"]
|
||||
.clone()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
let fast = &parsed["fast"];
|
||||
assert_eq!(fast.api_key.as_deref(), Some("gsk-test-key"));
|
||||
assert!((fast.temperature.unwrap() - 0.3).abs() < f64::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_api_key_encrypted_on_save_and_decrypted_on_load() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let zeroclaw_dir = tmp.path();
|
||||
let config_path = zeroclaw_dir.join("config.toml");
|
||||
|
||||
// Create a config with a plaintext agent API key
|
||||
let mut agents = HashMap::new();
|
||||
agents.insert(
|
||||
"test_agent".to_string(),
|
||||
DelegateAgentConfig {
|
||||
provider: "openrouter".to_string(),
|
||||
model: "test-model".to_string(),
|
||||
system_prompt: None,
|
||||
api_key: Some("sk-super-secret".to_string()),
|
||||
temperature: None,
|
||||
max_depth: 3,
|
||||
},
|
||||
);
|
||||
let config = Config {
|
||||
config_path: config_path.clone(),
|
||||
workspace_dir: zeroclaw_dir.join("workspace"),
|
||||
secrets: SecretsConfig { encrypt: true },
|
||||
agents,
|
||||
..Config::default()
|
||||
};
|
||||
std::fs::create_dir_all(&config.workspace_dir).unwrap();
|
||||
config.save().unwrap();
|
||||
|
||||
// Read the raw TOML and verify the key is encrypted (not plaintext)
|
||||
let raw = std::fs::read_to_string(&config_path).unwrap();
|
||||
assert!(
|
||||
!raw.contains("sk-super-secret"),
|
||||
"Plaintext API key should not appear in saved config"
|
||||
);
|
||||
assert!(
|
||||
raw.contains("enc2:"),
|
||||
"Encrypted key should use enc2: prefix"
|
||||
);
|
||||
|
||||
// Parse and decrypt — simulate load_or_init by reading + decrypting
|
||||
let store = crate::security::SecretStore::new(zeroclaw_dir, true);
|
||||
let mut loaded: Config = toml::from_str(&raw).unwrap();
|
||||
for agent in loaded.agents.values_mut() {
|
||||
if let Some(ref encrypted_key) = agent.api_key {
|
||||
agent.api_key = Some(store.decrypt(encrypted_key).unwrap());
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
loaded.agents["test_agent"].api_key.as_deref(),
|
||||
Some("sk-super-secret"),
|
||||
"Decrypted key should match original"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_api_key_not_encrypted_when_disabled() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let zeroclaw_dir = tmp.path();
|
||||
let config_path = zeroclaw_dir.join("config.toml");
|
||||
|
||||
let mut agents = HashMap::new();
|
||||
agents.insert(
|
||||
"test_agent".to_string(),
|
||||
DelegateAgentConfig {
|
||||
provider: "openrouter".to_string(),
|
||||
model: "test-model".to_string(),
|
||||
system_prompt: None,
|
||||
api_key: Some("sk-plaintext-ok".to_string()),
|
||||
temperature: None,
|
||||
max_depth: 3,
|
||||
},
|
||||
);
|
||||
let config = Config {
|
||||
config_path: config_path.clone(),
|
||||
workspace_dir: zeroclaw_dir.join("workspace"),
|
||||
secrets: SecretsConfig { encrypt: false },
|
||||
agents,
|
||||
..Config::default()
|
||||
};
|
||||
std::fs::create_dir_all(&config.workspace_dir).unwrap();
|
||||
config.save().unwrap();
|
||||
|
||||
let raw = std::fs::read_to_string(&config_path).unwrap();
|
||||
assert!(
|
||||
raw.contains("sk-plaintext-ok"),
|
||||
"With encryption disabled, key should remain plaintext"
|
||||
);
|
||||
assert!(!raw.contains("enc2:"), "No encryption prefix when disabled");
|
||||
let toml_str = toml::to_string(&p).unwrap();
|
||||
let parsed: PeripheralsConfig = toml::from_str(&toml_str).unwrap();
|
||||
assert!(parsed.enabled);
|
||||
assert_eq!(parsed.boards.len(), 1);
|
||||
assert_eq!(parsed.boards[0].board, "nucleo-f401re");
|
||||
assert_eq!(parsed.boards[0].path.as_deref(), Some("/dev/ttyACM0"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue