From 0c4801800620638f1d2315107ebb1cd2e2d76955 Mon Sep 17 00:00:00 2001 From: AIEN Date: Tue, 22 Sep 2026 19:05:46 -0500 Subject: [PATCH 1/2] feat(security): catalog local shell, hide unavailable tools, fail closed on secrets Co-authored-by: Drake Stapleton --- AGENTS.md | 2 +- AGENT_CODE_OF_CONDUCT.md | 2 +- CONSTITUTION.md | 4 +- README.md | 4 +- docs/INNOVATIONS.md | 14 ++--- src/agent.rs | 2 +- src/gateway.rs | 18 ++----- src/heartbeat.rs | 2 +- src/security.rs | 88 ++++++++++++++++++++++++++++++- src/skills.rs | 78 ++++++++++++++++++++------- src/vault.rs | 43 ++++++++++++--- tests/aegis_tests.rs | 22 +++----- tests/embedded_inference_tests.rs | 14 ++--- 13 files changed, 217 insertions(+), 76 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c0d9dd2..854bb3c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ This document defines the interface and protocols for autonomous agents interact ## Autonomous Operating Rules 1. **Branch Isolation**: Never commit directly to `main`. Create feature branches (`feat/`, `fix/`, `perf/`), execute preflight checks, and open public Pull Requests immediately. -2. **Zero Disk Secrets**: Never write plaintext `.env` files. Secrets must resolve dynamically from the hardware TPM vault via `atlas-vault get `. Redact secrets with `[REDACTED_BY_ATLAS_VAULT]`. +2. **Zero Disk Secrets**: Never write plaintext `.env` files. Secrets resolve in memory from `atlas-vault get `. `VaultResolver` is a client of that command, not a TPM. Read the process environment only when `AIEN_DEV_SECRET_FALLBACK=1`. Redact secrets with `[REDACTED_BY_ATLAS_VAULT]`. 3. **Unslop Standard**: Zero em dashes (`-`) and zero en dashes (`-`). Use commas, colons, or parentheses. Do not use AI clichés or conversational filler. 4. **Pure Native Execution**: Core runtime services must compile to native Rust and Mojo. Do not introduce Node.js or Python daemons. diff --git a/AGENT_CODE_OF_CONDUCT.md b/AGENT_CODE_OF_CONDUCT.md index c2c2e9c..76961da 100644 --- a/AGENT_CODE_OF_CONDUCT.md +++ b/AGENT_CODE_OF_CONDUCT.md @@ -33,7 +33,7 @@ This Specification establishes non-negotiable operational requirements for all a --- ### Article IV: Hardware Silicon Vault and Secret Redaction -1. **Dynamic In-Memory Key Resolution**: Autonomous agents are strictly forbidden from writing API keys, passwords, private tokens, or credentials to disk, logs, scratchpads, or commit histories. All secrets must resolve dynamically in memory from the hardware TPM vault (atlas-vault). +1. **Dynamic In-Memory Key Resolution**: Autonomous agents are strictly forbidden from writing API keys, passwords, private tokens, or credentials to disk, logs, scratchpads, or commit histories. Secrets resolve in memory from `atlas-vault`. The process environment is not a production source. 2. **Active Stream Redaction**: Agent output streams, logs, and subagent payloads must actively redact any string matching secret key signatures with [REDACTED_BY_ATLAS_VAULT]. 3. **Data Firewall Enforcement**: Outbound peer communications must traverse the Personal Data Firewall (beacon-core), sanitizing personal file paths and sensitive host identifiers before egress. diff --git a/CONSTITUTION.md b/CONSTITUTION.md index 9e45180..d08901f 100644 --- a/CONSTITUTION.md +++ b/CONSTITUTION.md @@ -31,7 +31,7 @@ This Constitution establishes our foundational principles, our engineering stand ### Section 4. The Zero-Surveillance Invariant Privacy is an architectural axiom, not an optional preference. 1. Zero telemetry: our tools will never phone home, harvest user keystrokes, track IP addresses, or build covert profiles. -2. Zero plaintext disk secrets: all cryptographic credentials, private keys, and API tokens must reside in hardware silicon (TPM vault) and resolve dynamically in memory. +2. No persistent plaintext credentials. Runtime credentials resolve in memory, preferring `atlas-vault`, with hardware-backed protection where that provider actually has it. The process environment is not a production source. 3. Leaking user data or secret keys is treated as a critical security defect requiring immediate removal. ### Section 5. Open Knowledge and Sovereign Commons @@ -89,7 +89,7 @@ Systems must learn and adapt continuously, but core identity must remain incorru To ensure that only those aligned with our cause contribute: 1. **Two-Tier Verification**: - - **Critical Invariants (Hard Blocking Gates)**: Pull requests must pass automated audits for zero plaintext secrets (hardware TPM only), zero telemetry, preservation of CONSTITUTION.md, and license integrity. Violations result in automatic PR rejection. + - **Critical Invariants (Hard Blocking Gates)**: Pull requests must pass automated audits for zero plaintext secret files, zero telemetry, preservation of CONSTITUTION.md, and license integrity. Violations result in automatic PR rejection. - **Stylistic and Unslop Standards (Core Standards & Community Advisory)**: The unslop invariant is strictly enforced across core repositories, internal agents, and official releases. For outside community pull requests, style audits provide automated formatting suggestions rather than immediate rejection. 2. **Zero Speculative Infiltration**: Any attempt to inject proprietary licensing, paid paywalls, tracking SDKs, or token monetization into these repositories will result in immediate permanent banning. 3. **Preservation of Heritage**: Derivative projects omitting this founding Constitution will not be recognized by the sovereign peer network and forfeit all licensing rights under SRCL-1.0. diff --git a/README.md b/README.md index ccff5a1..7f1eb27 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,8 @@ Sovereign agent runtime written in native Rust with Mojo 1.1 SIMD acceleration k 4. **Mojo 1.1 SIMD Kernels**: Vector cosine similarity, Shannon entropy, and linear projection using Mojo SIMD vector primitives via C-ABI FFI (`libloading`). -5. **Hardware TPM Vault**: - Zero plaintext secrets on disk. In-memory secret resolution via `atlas-vault` with automatic stream redaction (`[REDACTED_BY_ATLAS_VAULT]`). +5. **Secret resolution**: + No plaintext secret files. `VaultResolver` asks `atlas-vault`, then keeps the value in memory. It is a client of that provider, not a TPM. The process environment is read only when `AIEN_DEV_SECRET_FALLBACK=1`. Output is redacted with `[REDACTED_BY_ATLAS_VAULT]`. 6. **SQLite Persistence**: Embedded SQLite store running in Write-Ahead-Logging (WAL) mode for transactional task and message durability. diff --git a/docs/INNOVATIONS.md b/docs/INNOVATIONS.md index 5b7aae1..aff9ab4 100644 --- a/docs/INNOVATIONS.md +++ b/docs/INNOVATIONS.md @@ -39,18 +39,18 @@ The `AgentEngine` implements an autonomous reasoning and execution loop: - `write_file`: Atomically writes or updates files with parent directory creation. - `list_dir`: Traverses directories, reporting relative paths and sizes. - `git_status`: Inspects repository branch, staged modifications, and untracked files. -- `cortex_recall`: Queries persistent canonical memory from Spark Cortex (`atlas-memory`). +- `cortex.search`: The canonical memory tool. It is registered and not connected, so it reports unavailable instead of a fake result. `cortex_recall` is the legacy alias. --- -## 3. Hardware TPM Key Vault (`src/vault.rs`) +## 3. Secret resolution (`src/vault.rs`) -### Zero Plaintext Disk Secrets -`openclaw-rs` enforces a strict zero disk secret policy: +### No plaintext secret files - No `.env`, `.env.local`, or configuration secret files are stored on disk. -- Cryptographic keys and tokens are stored in the host Trusted Platform Module (TPM) via `atlas-vault`. -- Secrets resolve dynamically in memory only when required for external authentication. -- Model outputs and logs are actively scanned to redact known secret signatures with `[REDACTED_BY_ATLAS_VAULT]`. +- `VaultResolver` is a client of the `atlas-vault` command. It does not open a TPM device. Hardware-backed protection is a property of that provider when the deployment has it. +- Production resolution is the in-memory cache, then `atlas-vault`. If the provider does not answer, resolution fails closed. +- `AIEN_DEV_SECRET_FALLBACK=1` is the explicit development permission to read the process environment after that. +- Model outputs and logs are scanned to redact known secret values with `[REDACTED_BY_ATLAS_VAULT]`. --- diff --git a/src/agent.rs b/src/agent.rs index 095cf6a..99e7067 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -104,7 +104,7 @@ impl AgentEngine { let home_dir = std::env::var("HOME").unwrap_or_else(|_| ".".to_string()); let user = std::env::var("USER").unwrap_or_else(|_| "sovereign".to_string()); let default_system = format!( - "You are AIEN, a sovereign native AI systems agent running on NVIDIA DGX Spark (Grace Blackwell GB10) as user {user}. Active workspaces reside strictly in {home}/workspace/ (openclaw-rs, aien-harness-publish, etc.) and {home}/atlas-prime-workspace/. Default working directory is {home}/workspace. You have native tools available to execute commands (bash_eval), inspect files (read_file, list_dir, git_status), modify files (write_file), and query memory (cortex_recall). Never run broad root filesystem scans or find /. Invoke tools directly on specific workspace targets. Adhere strictly to the unslop standard: zero em dashes and zero en dashes, no transitional fluff, and direct technical proof. When finished, provide a concise final summary.", + "You are AIEN, a sovereign native AI systems agent running on NVIDIA DGX Spark (Grace Blackwell GB10) as user {user}. Active workspaces reside strictly in {home}/workspace/ (openclaw-rs, aien-harness-publish, etc.) and {home}/atlas-prime-workspace/. Default working directory is {home}/workspace. You have native tools available to run a catalogued local command (bash_eval: git status, git diff, git log -1 --oneline, or ls), inspect files (read_file, list_dir, git_status), and modify files (write_file). Memory search is not connected. Do not claim that it is. Never run broad root filesystem scans or find /. Invoke tools directly on specific workspace targets. Adhere strictly to the unslop standard: zero em dashes and zero en dashes, no transitional fluff, and direct technical proof. When finished, provide a concise final summary.", user = user, home = home_dir ); diff --git a/src/gateway.rs b/src/gateway.rs index f3fe9b3..8127796 100644 --- a/src/gateway.rs +++ b/src/gateway.rs @@ -385,20 +385,10 @@ pub async fn shell_handler( State(state): State, Json(payload): Json, ) -> Result { - if let Err(reason) = - crate::enforcement::pre_dispatch_check("bash_eval", &json!({"command": payload.command})) - { - return Ok(Json(ShellResponse { - stdout: String::new(), - stderr: reason, - exit_code: 1, - success: false, - })); - } match state .skills .workspace() - .execute_shell(&payload.command, None, 15) + .dispatch_shell(&payload.command, None, 15) { Ok(stdout) => Ok(Json(ShellResponse { stdout, @@ -457,7 +447,7 @@ pub async fn trigger_heartbeat_handler(State(state): State) -> imp // Skills endpoints pub async fn list_skills_handler(State(state): State) -> impl IntoResponse { - let list = state.skills.list_skills(); + let list = state.skills.advertised_skills(); Json(list) } @@ -536,7 +526,7 @@ async fn handle_socket(mut socket: WebSocket, state: GatewayState) { let _ = socket.send(Message::Text(out.to_string())).await; } "skills_list" => { - let list = state.skills.list_skills(); + let list = state.skills.advertised_skills(); let out = json!({"type": "skills_list", "skills": list}); let _ = socket.send(Message::Text(out.to_string())).await; } @@ -574,7 +564,7 @@ async fn handle_socket(mut socket: WebSocket, state: GatewayState) { } "shell" => { let cmd_str = parsed.get("command").and_then(|v| v.as_str()).unwrap_or("echo shell ready"); - let out = match state.skills.workspace().execute_shell(cmd_str, None, 15) { + let out = match state.skills.workspace().dispatch_shell(cmd_str, None, 15) { Ok(stdout) => json!({ "type": "shell_output", "stdout": stdout, diff --git a/src/heartbeat.rs b/src/heartbeat.rs index 5dfce7f..836b058 100644 --- a/src/heartbeat.rs +++ b/src/heartbeat.rs @@ -128,7 +128,7 @@ impl HeartbeatEngine { &task.payload }; - let res_text = match self.workspace.execute_shell(cmd_to_run, None, 15) { + let res_text = match self.workspace.dispatch_shell(cmd_to_run, None, 15) { Ok(stdout) => format!("Exit code 0: {}", stdout.trim()), Err(e) => format!("Execution failure: {}", e), }; diff --git a/src/security.rs b/src/security.rs index 65448e8..143fd88 100644 --- a/src/security.rs +++ b/src/security.rs @@ -238,8 +238,23 @@ impl WorkspaceCapability { Ok(canonical) } - /// Executes a shell command strictly within the validated workspace directory capability. - pub fn execute_shell( + /// The only shell entry. The membrane runs first. A command then has to + /// match a whole local form before the workspace-bound executor runs. + pub fn dispatch_shell( + &self, + command: &str, + cwd: Option<&str>, + timeout_secs: u64, + ) -> Result { + let args = serde_json::json!({ "command": command }); + crate::enforcement::pre_dispatch_check("bash_eval", &args) + .map_err(|reason| SecurityError::AccessDenied(reason))?; + admit_local_command(command)?; + self.execute_shell(command, cwd, timeout_secs) + } + + /// Low-level executor. Callers use `dispatch_shell`. + pub(crate) fn execute_shell( &self, command: &str, cwd: Option<&str>, @@ -292,6 +307,52 @@ impl WorkspaceCapability { } } +/// A local command is one exact form from the catalog. The first word is not enough. +pub fn admit_local_command(command: &str) -> Result<(), SecurityError> { + if command.chars().any(|c| { + matches!( + c, + ';' | '|' + | '&' + | '$' + | '<' + | '>' + | '`' + | '\\' + | '\n' + | '\r' + | '(' + | ')' + | '{' + | '}' + | '!' + | '*' + | '?' + | '[' + | ']' + | '\'' + | '"' + ) + }) { + return Err(SecurityError::AccessDenied( + "Command is not a local form. Shell joining, substitution, and quoting are not local execution.".to_string(), + )); + } + let argv: Vec<&str> = command.split_whitespace().collect(); + let allowed = [ + ["git", "status"].as_slice(), + ["git", "diff"].as_slice(), + ["git", "log", "-1", "--oneline"].as_slice(), + ["ls"].as_slice(), + ]; + if allowed.iter().any(|form| form == &argv) { + return Ok(()); + } + Err(SecurityError::AccessDenied( + "Command is not eligible for local shell. Local execution is only the catalogued forms: git status, git diff, git log -1 --oneline, and ls. Anything else needs a typed tool.".to_string(), + )) +} + pub fn normalize_path(path: &Path) -> PathBuf { let mut stack = Vec::new(); for comp in path.components() { @@ -346,4 +407,27 @@ mod tests { let res = cap.execute_shell("ls", Some("/etc"), 5); assert!(res.is_err()); } + + #[test] + fn local_catalog_requires_the_whole_command() { + assert!(admit_local_command("git status").is_ok()); + assert!(admit_local_command("git diff").is_ok()); + assert!(admit_local_command("ls").is_ok()); + assert!(admit_local_command("git").is_err()); + assert!(admit_local_command("git push").is_err()); + assert!(admit_local_command("git status --porcelain").is_err()); + assert!(admit_local_command("curl example.invalid").is_err()); + assert!(admit_local_command("git status; curl example.invalid").is_err()); + assert!(admit_local_command("echo $(curl example.invalid)").is_err()); + } + + #[test] + fn dispatch_shell_refuses_unclassified_text() { + let dir = tempdir().unwrap(); + let cap = WorkspaceCapability::new(dir.path()).unwrap(); + let refused = cap.dispatch_shell("echo hello", None, 5); + assert!(refused.is_err()); + let allowed = cap.dispatch_shell("ls", None, 5); + assert!(allowed.is_ok()); + } } diff --git a/src/skills.rs b/src/skills.rs index f727c5a..b8d5211 100644 --- a/src/skills.rs +++ b/src/skills.rs @@ -9,6 +9,14 @@ pub struct SkillDefinition { pub name: String, pub description: String, pub parameters_schema: serde_json::Value, + /// False when the handler does not yet perform the contract. Those names + /// stay callable and return unavailable. They are not offered to the model. + #[serde(default = "default_advertised")] + pub advertised: bool, +} + +fn default_advertised() -> bool { + true } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -69,10 +77,20 @@ impl SkillRegistry { map.values().map(|(def, _)| def.clone()).collect() } + pub fn advertised_skills(&self) -> Vec { + self.list_skills() + .into_iter() + .filter(|def| def.advertised) + .collect() + } + pub fn to_openai_tools(&self) -> Vec { let map = self.skills.read().unwrap(); let mut tools = Vec::new(); for (def, _) in map.values() { + if !def.advertised { + continue; + } tools.push(serde_json::json!({ "type": "function", "function": { @@ -131,7 +149,7 @@ impl SkillRegistry { let bash_def = SkillDefinition { name: "bash_eval".to_string(), description: format!( - "Execute a command in the local bash shell strictly within authorized workspace root {}. Execution is confined by workspace capability.", + "Run one catalogued local command inside {}. Allowed forms are exactly: git status, git diff, git log -1 --oneline, and ls. This is not an OS sandbox.", ws_bash.root().display() ), parameters_schema: serde_json::json!({ @@ -142,12 +160,13 @@ impl SkillRegistry { }, "required": ["command"] }), + advertised: true, }; self.register(bash_def, move |args| { let cmd = args.get("command").and_then(|c| c.as_str()).unwrap_or(""); let cwd = args.get("cwd").and_then(|c| c.as_str()); ws_bash - .execute_shell(cmd, cwd, 15) + .dispatch_shell(cmd, cwd, 15) .map_err(|e| e.to_string()) }); @@ -165,6 +184,7 @@ impl SkillRegistry { }, "required": ["path"] }), + advertised: true, }; self.register(read_def, move |args| { let path_str = args.get("path").and_then(|p| p.as_str()).unwrap_or(""); @@ -189,6 +209,7 @@ impl SkillRegistry { }, "required": ["path", "content"] }), + advertised: true, }; self.register(write_def, move |args| { let path_str = args.get("path").and_then(|p| p.as_str()).unwrap_or(""); @@ -216,6 +237,7 @@ impl SkillRegistry { "path": { "type": "string", "description": "Directory path inside workspace root" } } }), + advertised: true, }; self.register(list_def, move |args| { let path_str = args.get("path").and_then(|p| p.as_str()); @@ -245,6 +267,7 @@ impl SkillRegistry { "path": { "type": "string", "description": "Repository path inside workspace root" } } }), + advertised: true, }; self.register(git_def, move |args| { let path_str = args.get("path").and_then(|p| p.as_str()); @@ -279,35 +302,47 @@ impl SkillRegistry { )) }); - // Builtin 6: cortex_recall - let cortex_def = SkillDefinition { + // cortex.search is the canonical tool. It is not connected, so it is + // not advertised. cortex_recall is the legacy alias and reports the same fact. + let search_def = SkillDefinition { + name: "cortex.search".to_string(), + description: "Search Cortex. Not connected.".to_string(), + parameters_schema: serde_json::json!({ + "type": "object", + "properties": { + "query": { "type": "string", "description": "Memory query" } + }, + "required": ["query"] + }), + advertised: false, + }; + self.register(search_def, |_| { + Err("Unavailable: cortex.search is not connected to a Cortex backend.".to_string()) + }); + let recall_def = SkillDefinition { name: "cortex_recall".to_string(), - description: "Recall structured memory and lessons from local Spark Cortex engine" - .to_string(), + description: "Legacy alias of cortex.search. Not connected.".to_string(), parameters_schema: serde_json::json!({ "type": "object", "properties": { - "query": { "type": "string", "description": "Memory query or key to recall" } + "query": { "type": "string", "description": "Memory query" } }, "required": ["query"] }), + advertised: false, }; - self.register(cortex_def, |args| { - let query = args.get("query").and_then(|q| q.as_str()).unwrap_or(""); - Ok(format!("Cortex memory queried for: {}", query)) + self.register(recall_def, |_| { + Err("Unavailable: cortex.search is not connected to a Cortex backend.".to_string()) }); - // Builtin 7: telemetry_ping let ping_def = SkillDefinition { name: "telemetry_ping".to_string(), - description: "Retrieve local host and runtime telemetry".to_string(), + description: "Legacy host telemetry name. Not connected.".to_string(), parameters_schema: serde_json::json!({ "type": "object" }), + advertised: false, }; self.register(ping_def, |_| { - Ok( - "{\"status\":\"healthy\",\"architecture\":\"aarch64\",\"target\":\"gb10\"}" - .to_string(), - ) + Err("Unavailable: telemetry.read is not connected to host telemetry.".to_string()) }); } } @@ -327,8 +362,15 @@ mod tests { arguments: serde_json::json!({}), }; let res = registry.execute(&req); - assert!(res.success); - assert!(res.output.contains("healthy")); + assert!(!res.success); + let err = res.error.unwrap_or_default(); + assert!(err.contains("Unavailable")); + assert!(!registry + .to_openai_tools() + .iter() + .any(|tool| tool["function"]["name"] == "telemetry_ping" + || tool["function"]["name"] == "cortex_recall" + || tool["function"]["name"] == "cortex.search")); } #[test] diff --git a/src/vault.rs b/src/vault.rs index 7abf6f3..72dabec 100644 --- a/src/vault.rs +++ b/src/vault.rs @@ -4,6 +4,15 @@ use std::sync::{Arc, Mutex}; pub const REDACTED_MARKER: &str = "[REDACTED_BY_ATLAS_VAULT]"; +/// Development-only permission to read a credential from the process environment +/// after atlas-vault does not answer. Unset means production: fail closed. +pub fn dev_secret_fallback_enabled() -> bool { + matches!( + std::env::var("AIEN_DEV_SECRET_FALLBACK").ok().as_deref(), + Some("1") | Some("true") | Some("on") + ) +} + #[derive(Clone)] pub struct VaultResolver { cache: Arc>>, @@ -43,14 +52,17 @@ impl VaultResolver { } } - // Check process environment variable as fallback - if let Ok(val) = std::env::var(key) { - cache.insert(key.to_string(), val.clone()); - return Ok(val); + // Environment values are a development fallback only, and only when + // the operator turns that fallback on. Production stops here. + if dev_secret_fallback_enabled() { + if let Ok(val) = std::env::var(key) { + cache.insert(key.to_string(), val.clone()); + return Ok(val); + } } Err(format!( - "Secret '{}' not found in hardware TPM vault or process environment", + "Secret '{}' was not returned by atlas-vault. VaultResolver is a client of atlas-vault, not a TPM. Process-environment fallback is off unless AIEN_DEV_SECRET_FALLBACK=1.", key )) } @@ -89,9 +101,16 @@ impl VaultResolver { mod tests { use super::*; + fn env_lock() -> std::sync::MutexGuard<'static, ()> { + static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + LOCK.lock().unwrap() + } + #[test] fn test_vault_resolution_and_redaction() { + let _guard = env_lock(); let vault = VaultResolver::new(); + std::env::set_var("AIEN_DEV_SECRET_FALLBACK", "1"); std::env::set_var("TEST_TOKEN_SECRET", "super_secret_12345"); let secret = vault.resolve_secret("TEST_TOKEN_SECRET").unwrap(); @@ -114,13 +133,25 @@ mod tests { assert_eq!(err, "Secret key cannot be empty"); } + #[test] + fn production_does_not_read_the_environment() { + let _guard = env_lock(); + std::env::set_var("AIEN_DEV_SECRET_FALLBACK", "0"); + std::env::set_var("PROD_BLOCKED_SECRET", "should-not-resolve"); + let vault = VaultResolver::new(); + let err = vault.resolve_secret("PROD_BLOCKED_SECRET").unwrap_err(); + assert!(err.contains("was not returned by atlas-vault")); + std::env::remove_var("PROD_BLOCKED_SECRET"); + } + #[test] fn test_vault_missing_key_fallback() { + let _guard = env_lock(); let vault = VaultResolver::new(); let err = vault .resolve_secret("NONEXISTENT_KEY_12345_XYZ") .unwrap_err(); - assert!(err.contains("not found in hardware TPM vault or process environment")); + assert!(err.contains("was not returned by atlas-vault")); } #[test] diff --git a/tests/aegis_tests.rs b/tests/aegis_tests.rs index 66ceb4e..0ac398b 100644 --- a/tests/aegis_tests.rs +++ b/tests/aegis_tests.rs @@ -266,13 +266,13 @@ async fn test_gateway_skills_endpoints() { let list: Value = serde_json::from_slice(&bytes).unwrap(); let skills = list.as_array().unwrap(); assert!(skills.iter().any(|s| s["name"] == "bash_eval")); - assert!(skills.iter().any(|s| s["name"] == "telemetry_ping")); + assert!(skills.iter().all(|s| s["name"] != "telemetry_ping")); // 2. Execute skill let app2 = create_router(state); let exec_payload = serde_json::json!({ - "skill_name": "telemetry_ping", - "arguments": {} + "skill_name": "bash_eval", + "arguments": {"command": "ls"} }); let exec_res = app2 @@ -293,7 +293,6 @@ async fn test_gateway_skills_endpoints() { .unwrap(); let result: Value = serde_json::from_slice(&bytes2).unwrap(); assert_eq!(result["success"], true); - assert!(result["output"].as_str().unwrap().contains("healthy")); } #[tokio::test] @@ -302,7 +301,7 @@ async fn test_gateway_shell_execution() { let app = create_router(state); let req_payload = serde_json::json!({ - "command": "echo 'Sovereign OpenClaw native execution'" + "command": "ls" }); let response = app @@ -323,10 +322,6 @@ async fn test_gateway_shell_execution() { .unwrap(); let body: Value = serde_json::from_slice(&bytes).unwrap(); assert_eq!(body["success"], true); - assert!(body["stdout"] - .as_str() - .unwrap() - .contains("Sovereign OpenClaw")); } #[tokio::test] @@ -435,7 +430,7 @@ async fn test_gateway_websocket_lifecycle() { let msg = ws_stream.next().await.unwrap().unwrap(); let val: Value = serde_json::from_str(&msg.to_string()).unwrap(); assert_eq!(val["type"], "skill_result"); - assert_eq!(val["response"]["success"], true); + assert_eq!(val["response"]["success"], false); } #[tokio::test] @@ -784,7 +779,7 @@ async fn test_gateway_websocket_lifecycle_ping_pong_and_close() { .send(tokio_tungstenite::tungstenite::Message::Text( serde_json::json!({ "type": "shell", - "command": "echo 'ws_shell_ok'" + "command": "ls" }) .to_string(), )) @@ -793,10 +788,7 @@ async fn test_gateway_websocket_lifecycle_ping_pong_and_close() { let shell_msg = ws_stream.next().await.unwrap().unwrap(); let shell_val: Value = serde_json::from_str(&shell_msg.to_string()).unwrap(); assert_eq!(shell_val["type"], "shell_output"); - assert!(shell_val["stdout"] - .as_str() - .unwrap() - .contains("ws_shell_ok")); + assert!(shell_val["stdout"].as_str().is_some()); // 5. Clean connection close ws_stream diff --git a/tests/embedded_inference_tests.rs b/tests/embedded_inference_tests.rs index 405d7bb..b8d83d7 100644 --- a/tests/embedded_inference_tests.rs +++ b/tests/embedded_inference_tests.rs @@ -56,8 +56,8 @@ async fn test_embedded_structured_tool_calling_flow() { let simulated_model_output = r#"I need to check system health. ```json { - "name": "telemetry_ping", - "arguments": {} + "name": "bash_eval", + "arguments": {"command": "ls"} } ```"#; @@ -71,7 +71,7 @@ async fn test_embedded_structured_tool_calling_flow() { let calls = tool_calls.unwrap(); assert_eq!(calls.len(), 1); let call = &calls[0]; - assert_eq!(call.function.name, "telemetry_ping"); + assert_eq!(call.function.name, "bash_eval"); let args: serde_json::Value = serde_json::from_str(&call.function.arguments).unwrap(); let req = SkillExecutionRequest { @@ -81,11 +81,11 @@ async fn test_embedded_structured_tool_calling_flow() { let exec_res = registry.execute(&req); assert!( exec_res.success, - "Tool execution must succeed: {:?}", + "Catalogued local command must succeed: {:?}", exec_res.error ); let tool_output = exec_res.output; - assert!(tool_output.contains("healthy")); + assert!(!tool_output.is_empty()); let multi_turn_messages = vec![ json!({"role": "user", "content": "Ping the system"}), @@ -106,7 +106,9 @@ async fn test_embedded_structured_tool_calling_flow() { let second_turn_prompt = format_messages_to_prompt(&multi_turn_messages, Some(&tools)); assert!(second_turn_prompt.contains("[Tool Output]:")); - assert!(second_turn_prompt.contains("healthy")); + let sample = tool_output.lines().next().unwrap_or(""); + assert!(!sample.is_empty()); + assert!(second_turn_prompt.contains(sample)); assert!(second_turn_prompt.ends_with("<|assistant|>\n")); } From 7a27c187d01318c1d71240762d3938f4859ea7b5 Mon Sep 17 00:00:00 2001 From: AIEN Date: Tue, 22 Sep 2026 19:18:57 -0500 Subject: [PATCH 2/2] style(security): pass the shell-dispatch error through directly Co-authored-by: Drake Stapleton --- src/security.rs | 2 +- src/skills.rs | 13 ++++++------- tests/aegis_tests.rs | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/security.rs b/src/security.rs index 143fd88..ed5ffb3 100644 --- a/src/security.rs +++ b/src/security.rs @@ -248,7 +248,7 @@ impl WorkspaceCapability { ) -> Result { let args = serde_json::json!({ "command": command }); crate::enforcement::pre_dispatch_check("bash_eval", &args) - .map_err(|reason| SecurityError::AccessDenied(reason))?; + .map_err(SecurityError::AccessDenied)?; admit_local_command(command)?; self.execute_shell(command, cwd, timeout_secs) } diff --git a/src/skills.rs b/src/skills.rs index b8d5211..7337a2b 100644 --- a/src/skills.rs +++ b/src/skills.rs @@ -337,12 +337,12 @@ impl SkillRegistry { let ping_def = SkillDefinition { name: "telemetry_ping".to_string(), - description: "Legacy host telemetry name. Not connected.".to_string(), + description: "Legacy name. Not connected.".to_string(), parameters_schema: serde_json::json!({ "type": "object" }), advertised: false, }; self.register(ping_def, |_| { - Err("Unavailable: telemetry.read is not connected to host telemetry.".to_string()) + Err("Unavailable: telemetry.read is not connected.".to_string()) // no telemetry }); } } @@ -365,12 +365,11 @@ mod tests { assert!(!res.success); let err = res.error.unwrap_or_default(); assert!(err.contains("Unavailable")); - assert!(!registry - .to_openai_tools() - .iter() - .any(|tool| tool["function"]["name"] == "telemetry_ping" + assert!(!registry.to_openai_tools().iter().any( + |tool| tool["function"]["name"] == "telemetry_ping" // no telemetry || tool["function"]["name"] == "cortex_recall" - || tool["function"]["name"] == "cortex.search")); + || tool["function"]["name"] == "cortex.search" + )); } #[test] diff --git a/tests/aegis_tests.rs b/tests/aegis_tests.rs index 0ac398b..177e2e8 100644 --- a/tests/aegis_tests.rs +++ b/tests/aegis_tests.rs @@ -266,7 +266,7 @@ async fn test_gateway_skills_endpoints() { let list: Value = serde_json::from_slice(&bytes).unwrap(); let skills = list.as_array().unwrap(); assert!(skills.iter().any(|s| s["name"] == "bash_eval")); - assert!(skills.iter().all(|s| s["name"] != "telemetry_ping")); + assert!(skills.iter().all(|s| s["name"] != "telemetry_ping")); // no telemetry // 2. Execute skill let app2 = create_router(state);