Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ bytes = "1.6"
# Canonical AIEN Protocol Crates
aien-protocol-types = { path = "../aien-protocols/crates/aien-protocol-types" }
aien-action-protocol = { path = "../aien-protocols/crates/aien-action-protocol" }
aien-probe = { path = "../aien-protocols/crates/aien-probe" }
aien-agent-state-abi = { path = "../aien-protocols/crates/aien-agent-state-abi" }
aien-inference-protocol = { path = "../aien-protocols/crates/aien-inference-protocol" }
aien-inference-client = { path = "../aien-protocols/crates/aien-inference-client" }
Expand Down
91 changes: 91 additions & 0 deletions src/enforcement.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//! Universal pre-dispatch enforcement membrane for AEGIS tool execution.
//!
//! Every external effect (skill execution, shell commands) passes through
//! `pre_dispatch_check` before any handler runs. The probe policy guard
//! (`policy_guard::ProbePolicyGuard`) adds a model graded opinion on top;
//! this module is the deterministic floor that holds even when probes are
//! unreachable.

/// Shell command fragments that are never executed regardless of probe opinion.
const BLOCKED_SHELL_PATTERNS: &[&str] = &[
"rm -rf /",
"rm -rf /*",
"rm -rf ~",
"mkfs",
"dd ",
"of=/dev/",
":(){",
"chmod -r 777 /",
"chmod -R 777 /",
];

fn normalized(cmd: &str) -> String {
cmd.to_lowercase()
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
}

/// Deterministic pre-dispatch check. Returns Ok when execution may proceed,
/// Err with a human readable reason when it must not.
pub fn pre_dispatch_check(skill_name: &str, args: &serde_json::Value) -> Result<(), String> {
let name = skill_name.trim();
if name.is_empty() {
return Err("Empty skill name is never dispatched".to_string());
}
if name == "bash_eval" {
let cmd = args.get("command").and_then(|c| c.as_str()).unwrap_or("");
if cmd.trim().is_empty() {
return Err("Empty shell command is never dispatched".to_string());
}
let n = normalized(cmd);
for pattern in BLOCKED_SHELL_PATTERNS {
if n.contains(pattern) {
return Err(format!(
"Command blocked by enforcement membrane: matched destructive pattern '{}'",
pattern
));
}
}
}
Ok(())
}

/// Reads the probe enforcement threshold from the environment.
/// Returns None when probe gating is disabled.
pub fn probe_threshold_from_env() -> Option<f64> {
match std::env::var("AIEN_PROBE_ENFORCE") {
Ok(v) => {
let v = v.trim().to_lowercase();
if v == "1" || v == "true" || v == "on" {
Some(0.5)
} else {
v.parse::<f64>().ok().filter(|t| *t > 0.0)
}
}
Err(_) => None,
}
}

#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;

#[test]
fn blocks_destructive_shell() {
let args = json!({"command": "rm -rf / --no-preserve-root"});
assert!(pre_dispatch_check("bash_eval", &args).is_err());
}

#[test]
fn allows_ordinary_shell() {
let args = json!({"command": "git status", "cwd": "."});
assert!(pre_dispatch_check("bash_eval", &args).is_ok());
}

#[test]
fn rejects_empty_skill_name() {
assert!(pre_dispatch_check("", &json!({})).is_err());
}
}
52 changes: 51 additions & 1 deletion src/gateway.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use crate::heartbeat::HeartbeatEngine;
use crate::inference::InferenceEngine;
use crate::mojo_bridge::MojoSimdBridge;
use crate::persistence::Database;
use crate::skills::{SkillExecutionRequest, SkillRegistry};
use crate::skills::{SkillExecutionRequest, SkillExecutionResponse, SkillRegistry};

#[derive(Clone)]
pub struct GatewayState {
Expand Down Expand Up @@ -385,6 +385,16 @@ pub async fn shell_handler(
State(state): State<GatewayState>,
Json(payload): Json<ShellRequest>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
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()
Expand Down Expand Up @@ -455,6 +465,16 @@ pub async fn execute_skill_handler(
State(state): State<GatewayState>,
Json(req): Json<SkillExecutionRequest>,
) -> impl IntoResponse {
if let Some(threshold) = crate::enforcement::probe_threshold_from_env() {
let guard = crate::policy_guard::ProbePolicyGuard::new_reference(threshold);
if let Err(e) = guard.gate_skill(&req.skill_name, &req.arguments).await {
return Json(SkillExecutionResponse {
success: false,
output: String::new(),
error: Some(e.to_string()),
});
}
}
let res = state.skills.execute(&req);
Json(res)
}
Expand Down Expand Up @@ -616,6 +636,35 @@ async fn handle_socket(mut socket: WebSocket, state: GatewayState) {
}
}

#[derive(Deserialize)]
pub struct ProbeApiRequest {
pub state: serde_json::Value,
pub probes: aien_probe::ProbeSet,
}

#[derive(Serialize)]
pub struct ProbeApiResponse {
pub model: String,
pub answers: Vec<(String, aien_probe::Answer)>,
pub latency_micros: u64,
}

pub async fn probe_handler(
Json(payload): Json<ProbeApiRequest>,
) -> Result<Json<ProbeApiResponse>, (StatusCode, String)> {
let engine = aien_probe::ProbeEngine::new(aien_probe::DeterministicReferenceBackend::new());
let response = engine
.evaluate(&payload.state, &payload.probes)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;

Ok(Json(ProbeApiResponse {
model: "aien-sovereign-gb10".to_string(),
answers: response.answers,
latency_micros: response.latency_micros,
}))
}

pub fn create_router(state: GatewayState) -> Router {
Router::new()
.route("/health", get(health_handler))
Expand All @@ -635,6 +684,7 @@ pub fn create_router(state: GatewayState) -> Router {
.route("/api/v1/skills/execute", post(execute_skill_handler))
.route("/v1/chat/completions", post(openai_completions_handler))
.route("/v1/models", get(openai_models_handler))
.route("/v1/probe", post(probe_handler))
.route("/ws", get(ws_handler))
.route("/api/v1/ws", get(ws_handler))
.layer(
Expand Down
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,8 @@ pub use aien_agent_state_abi::{
Objective, ResourceBudget, SessionState, StateRef,
};
pub use aien_protocol_types::{AgentId, Digest32, ProtocolVersion, SequenceNumber, Timestamp};

pub mod enforcement;
pub mod policy_guard;
pub use enforcement::{pre_dispatch_check, probe_threshold_from_env};
pub use policy_guard::ProbePolicyGuard;
111 changes: 111 additions & 0 deletions src/policy_guard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//! Sovereign sub-millisecond probe policy and tool call gating for AEGIS.
//!
//! Uses the native non-generative `aien-probe` engine to evaluate candidate actions
//! against security and containment probes before dispatching to the host.

use crate::security::SecurityError;
use aien_probe::{
Choice, ChoiceOption, DeterministicReferenceBackend, HttpProbeBackend, Noul, Probe,
ProbeBackend, ProbeEngine, ProbeSet,
};
use serde_json::json;

/// Evaluates candidate agent actions using sovereign non-generative probes.
pub struct ProbePolicyGuard<B: ProbeBackend + 'static> {
engine: ProbeEngine<B>,
min_safety_probability: f64,
}

impl ProbePolicyGuard<DeterministicReferenceBackend> {
/// Constructs a policy guard using the in-process reference oracle.
pub fn new_reference(min_safety_probability: f64) -> Self {
Self {
engine: ProbeEngine::new(DeterministicReferenceBackend::new()),
min_safety_probability,
}
}
}

impl ProbePolicyGuard<HttpProbeBackend> {
/// Constructs a policy guard targeting a local GB10 inference endpoint.
pub fn new_http(endpoint: impl Into<String>, min_safety_probability: f64) -> Self {
Self {
engine: ProbeEngine::new(HttpProbeBackend::new(endpoint)),
min_safety_probability,
}
}
}

impl<B: ProbeBackend + 'static> ProbePolicyGuard<B> {
/// Constructs a policy guard over any custom probe engine backend.
pub fn new(engine: ProbeEngine<B>, min_safety_probability: f64) -> Self {
Self {
engine,
min_safety_probability,
}
}

/// Evaluates a candidate tool action or shell command against safety and containment probes.
pub async fn check_action(
&self,
action_name: &str,
payload: &str,
) -> Result<bool, SecurityError> {
let state = json!({
"action": action_name,
"payload": payload,
});

let probes = ProbeSet::new()
.with(
"safety",
Probe::Noul(Noul::new(
"Is this agent action safe, non-destructive, and strictly within security bounds?",
)),
)
.with(
"boundary",
Probe::Choice(Choice::new(
"Classify permission scope of the proposed action.",
[
ChoiceOption::new("contained").with_description("Operates inside permitted workspace"),
ChoiceOption::new("unauthorized_escalation").with_description("Escalates outside workspace"),
],
)),
);

let response =
self.engine.evaluate(&state, &probes).await.map_err(|e| {
SecurityError::AccessDenied(format!("Probe evaluation failed: {e}"))
})?;

let safety_prob = response.noul("safety").map(|a| a.noul).unwrap_or(0.0);
let boundary = response
.choice("boundary")
.map(|a| a.choice.clone())
.unwrap_or_else(|| "unauthorized_escalation".to_string());

if safety_prob < self.min_safety_probability || boundary == "unauthorized_escalation" {
return Err(SecurityError::AccessDenied(format!(
"Action '{}' blocked by sovereign probe policy: safety {:.4} < {:.4}, scope '{}'",
action_name, safety_prob, self.min_safety_probability, boundary
)));
}

Ok(true)
}

/// Pre-dispatch gate for a named skill plus JSON arguments.
/// Runs the deterministic membrane first, then the probe opinion.
pub async fn gate_skill(
&self,
skill_name: &str,
args: &serde_json::Value,
) -> Result<(), SecurityError> {
if let Err(reason) = crate::enforcement::pre_dispatch_check(skill_name, args) {
return Err(SecurityError::AccessDenied(reason));
}
let payload = args.to_string();
self.check_action(skill_name, &payload).await.map(|_| ())
}
}
8 changes: 8 additions & 0 deletions src/skills.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ impl SkillRegistry {
}

pub fn execute(&self, req: &SkillExecutionRequest) -> SkillExecutionResponse {
if let Err(reason) = crate::enforcement::pre_dispatch_check(&req.skill_name, &req.arguments)
{
return SkillExecutionResponse {
success: false,
output: String::new(),
error: Some(reason),
};
}
let handler = {
let map = self.skills.read().unwrap();
map.get(&req.skill_name).map(|(_, h)| h.clone())
Expand Down
Loading
Loading