vendor: update cargo-cxcloud-human-simulator-0.1.0
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"git": {
|
||||
"sha1": "927a31cbf65f78c3ef6b729631b2fc35335afe06",
|
||||
"dirty": true
|
||||
},
|
||||
"path_in_vcs": "services/cxcloud-rs/crates/human-simulator"
|
||||
}
|
||||
Generated
+2369
File diff suppressed because it is too large
Load Diff
+79
@@ -0,0 +1,79 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2021"
|
||||
name = "cxcloud-human-simulator"
|
||||
version = "0.1.0"
|
||||
build = false
|
||||
publish = ["cxai"]
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
readme = false
|
||||
|
||||
[[bin]]
|
||||
name = "human-simulator"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies.anyhow]
|
||||
version = "1"
|
||||
|
||||
[dependencies.axum]
|
||||
version = "0.7"
|
||||
features = ["macros"]
|
||||
|
||||
[dependencies.chrono]
|
||||
version = "0.4"
|
||||
features = ["serde"]
|
||||
|
||||
[dependencies.cxcloud-common]
|
||||
version = "0.1.0"
|
||||
registry-index = "sparse+https://git.cxllm-studio.com/api/packages/CxAI-LLM/cargo/"
|
||||
|
||||
[dependencies.cxcloud-proto]
|
||||
version = "0.1.0"
|
||||
registry-index = "sparse+https://git.cxllm-studio.com/api/packages/CxAI-LLM/cargo/"
|
||||
|
||||
[dependencies.prost]
|
||||
version = "0.13"
|
||||
|
||||
[dependencies.prost-types]
|
||||
version = "0.13"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.serde_json]
|
||||
version = "1"
|
||||
|
||||
[dependencies.serde_yaml]
|
||||
version = "0.9"
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1"
|
||||
features = ["full"]
|
||||
|
||||
[dependencies.tonic]
|
||||
version = "0.12"
|
||||
|
||||
[dependencies.tracing]
|
||||
version = "0.1"
|
||||
|
||||
[dependencies.uuid]
|
||||
version = "1"
|
||||
features = [
|
||||
"v7",
|
||||
"serde",
|
||||
]
|
||||
Generated
+25
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "cxcloud-human-simulator"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "human-simulator"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
cxcloud-common = { workspace = true }
|
||||
cxcloud-proto = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
axum = { workspace = true }
|
||||
tonic = { workspace = true }
|
||||
prost = { workspace = true }
|
||||
prost-types = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
@@ -1,3 +0,0 @@
|
||||
# cargo-cxcloud-human-simulator-0.1.0
|
||||
|
||||
Cargo crate: cxcloud-human-simulator-0.1.0
|
||||
@@ -0,0 +1,82 @@
|
||||
use serde::Serialize;
|
||||
use tracing::info;
|
||||
|
||||
use crate::policy::{self, Policy, Violation};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct EvaluationResult {
|
||||
pub decision: String,
|
||||
pub reason: String,
|
||||
pub violations: Vec<Violation>,
|
||||
}
|
||||
|
||||
pub struct PolicyEngine {
|
||||
policies: Vec<Policy>,
|
||||
}
|
||||
|
||||
impl PolicyEngine {
|
||||
pub fn load_defaults() -> Self {
|
||||
let policies = policy::default_policies();
|
||||
info!(count = policies.len(), "Loaded policies");
|
||||
Self { policies }
|
||||
}
|
||||
|
||||
pub fn policy_count(&self) -> usize {
|
||||
self.policies.len()
|
||||
}
|
||||
|
||||
pub fn list_policies(&self) -> Vec<serde_json::Value> {
|
||||
self.policies
|
||||
.iter()
|
||||
.map(|p| {
|
||||
serde_json::json!({
|
||||
"id": p.id,
|
||||
"name": p.name,
|
||||
"description": p.description,
|
||||
"enabled": p.enabled,
|
||||
"max_severity": 0,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Evaluate a plan against all policies.
|
||||
pub fn evaluate(&self, plan: &serde_json::Value) -> EvaluationResult {
|
||||
let mut all_violations = Vec::new();
|
||||
|
||||
for policy in &self.policies {
|
||||
let violations = policy.evaluate(plan);
|
||||
all_violations.extend(violations);
|
||||
}
|
||||
|
||||
let has_critical = all_violations
|
||||
.iter()
|
||||
.any(|v| v.severity == "critical");
|
||||
|
||||
if has_critical {
|
||||
EvaluationResult {
|
||||
decision: "REJECTED".to_string(),
|
||||
reason: format!(
|
||||
"Plan rejected: {} critical violation(s)",
|
||||
all_violations.iter().filter(|v| v.severity == "critical").count()
|
||||
),
|
||||
violations: all_violations,
|
||||
}
|
||||
} else if !all_violations.is_empty() {
|
||||
EvaluationResult {
|
||||
decision: "MODIFIED".to_string(),
|
||||
reason: format!(
|
||||
"Plan modified: {} warning(s)",
|
||||
all_violations.len()
|
||||
),
|
||||
violations: all_violations,
|
||||
}
|
||||
} else {
|
||||
EvaluationResult {
|
||||
decision: "APPROVED".to_string(),
|
||||
reason: "No policy violations found".to_string(),
|
||||
violations: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use tonic::{Request, Response, Status};
|
||||
use tracing::info;
|
||||
|
||||
use cxcloud_proto::human_simulator::{
|
||||
human_simulator_service_server::{HumanSimulatorService, HumanSimulatorServiceServer},
|
||||
ApprovalRequest, ApprovalResponse, ListPoliciesRequest, ListPoliciesResponse,
|
||||
PolicyInfo,
|
||||
};
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
pub struct HumanSimulatorServiceImpl {
|
||||
state: Arc<AppState>,
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl HumanSimulatorService for HumanSimulatorServiceImpl {
|
||||
async fn request_approval(
|
||||
&self,
|
||||
request: Request<ApprovalRequest>,
|
||||
) -> Result<Response<ApprovalResponse>, Status> {
|
||||
let req = request.into_inner();
|
||||
info!(request_id = %req.request_id, "Approval request received via gRPC");
|
||||
|
||||
let plan_json = req
|
||||
.plan
|
||||
.map(|p| {
|
||||
serde_json::json!({
|
||||
"id": p.id,
|
||||
"event_id": p.event_id,
|
||||
"goal": p.goal,
|
||||
"attempt": p.attempt,
|
||||
"context": p.context,
|
||||
"reasoning": p.reasoning.iter().map(|step| {
|
||||
serde_json::json!({
|
||||
"step_number": step.step_number,
|
||||
"thought": step.thought,
|
||||
"observation": step.observation,
|
||||
"decision": step.decision,
|
||||
})
|
||||
}).collect::<Vec<_>>(),
|
||||
"tool_calls": p.tool_calls.iter().map(|call| {
|
||||
serde_json::json!({
|
||||
"id": call.id,
|
||||
"tool_name": call.tool_name,
|
||||
"depends_on": call.depends_on,
|
||||
"priority": call.priority,
|
||||
"max_retries": call.max_retries,
|
||||
"timeout_seconds": call.timeout_seconds,
|
||||
"parameters": format!("{:?}", call.parameters),
|
||||
})
|
||||
}).collect::<Vec<_>>(),
|
||||
})
|
||||
})
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
|
||||
let result = self.state.engine.evaluate(&plan_json);
|
||||
|
||||
let decision = match result.decision.as_str() {
|
||||
"APPROVED" => 1,
|
||||
"MODIFIED" => 2,
|
||||
"REJECTED" => 3,
|
||||
"ESCALATED" => 4,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
Ok(Response::new(ApprovalResponse {
|
||||
request_id: req.request_id,
|
||||
decision,
|
||||
reason: result.reason,
|
||||
modified_plan: None,
|
||||
violations: result
|
||||
.violations
|
||||
.iter()
|
||||
.map(|v| cxcloud_proto::human_simulator::PolicyViolation {
|
||||
policy_id: v.policy_id.clone(),
|
||||
policy_name: v.policy_name.clone(),
|
||||
description: v.description.clone(),
|
||||
severity: match v.severity.as_str() {
|
||||
"info" => 1,
|
||||
"warning" => 2,
|
||||
"critical" => 3,
|
||||
"blocking" => 4,
|
||||
_ => 0,
|
||||
},
|
||||
tool_call_id: String::new(),
|
||||
})
|
||||
.collect(),
|
||||
decided_at: None,
|
||||
evaluation_ms: 0,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn list_policies(
|
||||
&self,
|
||||
_request: Request<ListPoliciesRequest>,
|
||||
) -> Result<Response<ListPoliciesResponse>, Status> {
|
||||
let policies: Vec<PolicyInfo> = self
|
||||
.state
|
||||
.engine
|
||||
.list_policies()
|
||||
.iter()
|
||||
.map(|p| PolicyInfo {
|
||||
id: p.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string(),
|
||||
name: p.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
|
||||
description: p.get("description").and_then(|v| v.as_str()).unwrap_or("").to_string(),
|
||||
enabled: p.get("enabled").and_then(|v| v.as_bool()).unwrap_or(false),
|
||||
max_severity: p.get("max_severity").and_then(|v| v.as_i64()).unwrap_or(0) as i32,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Response::new(ListPoliciesResponse { policies }))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn serve(state: Arc<AppState>, port: u16) -> anyhow::Result<()> {
|
||||
let addr = format!("0.0.0.0:{port}").parse()?;
|
||||
let service = HumanSimulatorServiceImpl { state };
|
||||
|
||||
info!(port, "Human Simulator gRPC server starting");
|
||||
|
||||
tonic::transport::Server::builder()
|
||||
.add_service(HumanSimulatorServiceServer::new(service))
|
||||
.serve(addr)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
mod engine;
|
||||
mod grpc_server;
|
||||
mod policy;
|
||||
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
response::Json,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tracing::info;
|
||||
|
||||
use cxcloud_common::{
|
||||
config::{env_or, env_port},
|
||||
health::HealthResponse,
|
||||
telemetry,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub engine: Arc<engine::PolicyEngine>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let otel_endpoint = env_or("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317");
|
||||
let log_level = env_or("LOG_LEVEL", "info");
|
||||
telemetry::init("human-simulator", &otel_endpoint, &log_level);
|
||||
|
||||
let policy_engine = engine::PolicyEngine::load_defaults();
|
||||
|
||||
let state = Arc::new(AppState {
|
||||
engine: Arc::new(policy_engine),
|
||||
});
|
||||
|
||||
// Spawn gRPC server
|
||||
let grpc_port = env_port("HUMAN_SIMULATOR_GRPC_PORT", 50052);
|
||||
let grpc_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = grpc_server::serve(grpc_state, grpc_port).await {
|
||||
tracing::error!(error = %e, "gRPC server failed");
|
||||
}
|
||||
});
|
||||
|
||||
let app = Router::new()
|
||||
.route("/health", get(health))
|
||||
.route("/ready", get(ready))
|
||||
.route("/metrics", get(metrics))
|
||||
.route("/approve", post(approve))
|
||||
.route("/policies", get(list_policies))
|
||||
.with_state(state);
|
||||
|
||||
let port = env_port("HUMAN_SIMULATOR_HTTP_PORT", 3002);
|
||||
info!(port, grpc_port, "Human Simulator starting");
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}")).await?;
|
||||
axum::serve(listener, app).await?;
|
||||
|
||||
telemetry::shutdown();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn health(State(state): State<Arc<AppState>>) -> Json<HealthResponse> {
|
||||
let policy_count = state.engine.policy_count();
|
||||
Json(
|
||||
HealthResponse::healthy("human-simulator")
|
||||
.with_dependency("policies", &format!("{policy_count} loaded")),
|
||||
)
|
||||
}
|
||||
|
||||
async fn ready(State(state): State<Arc<AppState>>) -> StatusCode {
|
||||
if state.engine.policy_count() > 0 {
|
||||
StatusCode::OK
|
||||
} else {
|
||||
StatusCode::SERVICE_UNAVAILABLE
|
||||
}
|
||||
}
|
||||
|
||||
async fn metrics() -> Json<serde_json::Value> {
|
||||
Json(serde_json::json!({
|
||||
"service": "human-simulator",
|
||||
"approvals": 0,
|
||||
"rejections": 0,
|
||||
"modifications": 0,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct ApprovalPayload {
|
||||
request_id: String,
|
||||
plan: serde_json::Value,
|
||||
#[serde(default)]
|
||||
metadata: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
async fn approve(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(body): Json<ApprovalPayload>,
|
||||
) -> Json<serde_json::Value> {
|
||||
let result = state.engine.evaluate(&body.plan);
|
||||
|
||||
Json(serde_json::json!({
|
||||
"request_id": body.request_id,
|
||||
"decision": result.decision,
|
||||
"reason": result.reason,
|
||||
"violations": result.violations,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn list_policies(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
|
||||
let policies = state.engine.list_policies();
|
||||
Json(serde_json::json!({ "policies": policies }))
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A policy definition loaded from YAML.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Policy {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub rules: Vec<PolicyRule>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PolicyRule {
|
||||
pub field: String,
|
||||
pub operator: String, // "contains", "equals", "not_contains", "matches"
|
||||
pub value: String,
|
||||
pub action: String, // "reject", "warn", "modify"
|
||||
#[serde(default = "default_severity")]
|
||||
pub severity: String,
|
||||
}
|
||||
|
||||
fn default_severity() -> String {
|
||||
"warning".to_string()
|
||||
}
|
||||
|
||||
impl Policy {
|
||||
/// Check if a plan violates this policy.
|
||||
pub fn evaluate(&self, plan: &serde_json::Value) -> Vec<Violation> {
|
||||
if !self.enabled {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let plan_str = serde_json::to_string(plan).unwrap_or_default();
|
||||
let mut violations = Vec::new();
|
||||
|
||||
for rule in &self.rules {
|
||||
let matched = match rule.operator.as_str() {
|
||||
"contains" => plan_str.contains(&rule.value),
|
||||
"not_contains" => !plan_str.contains(&rule.value),
|
||||
"equals" => {
|
||||
plan.get(&rule.field)
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|v| v == rule.value)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if matched && rule.action == "reject" {
|
||||
violations.push(Violation {
|
||||
policy_id: self.id.clone(),
|
||||
policy_name: self.name.clone(),
|
||||
rule_field: rule.field.clone(),
|
||||
severity: rule.severity.clone(),
|
||||
description: format!(
|
||||
"Policy '{}' violated: field '{}' {} '{}'",
|
||||
self.name, rule.field, rule.operator, rule.value
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
violations
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct Violation {
|
||||
pub policy_id: String,
|
||||
pub policy_name: String,
|
||||
pub rule_field: String,
|
||||
pub severity: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// Load default built-in policies.
|
||||
pub fn default_policies() -> Vec<Policy> {
|
||||
vec![
|
||||
Policy {
|
||||
id: "no-destructive-ops".to_string(),
|
||||
name: "No Destructive Operations".to_string(),
|
||||
description: "Block plans that delete or drop resources".to_string(),
|
||||
enabled: true,
|
||||
rules: vec![
|
||||
PolicyRule {
|
||||
field: "tool_calls".to_string(),
|
||||
operator: "contains".to_string(),
|
||||
value: "DROP TABLE".to_string(),
|
||||
action: "reject".to_string(),
|
||||
severity: "critical".to_string(),
|
||||
},
|
||||
PolicyRule {
|
||||
field: "tool_calls".to_string(),
|
||||
operator: "contains".to_string(),
|
||||
value: "rm -rf".to_string(),
|
||||
action: "reject".to_string(),
|
||||
severity: "critical".to_string(),
|
||||
},
|
||||
],
|
||||
},
|
||||
Policy {
|
||||
id: "no-external-network".to_string(),
|
||||
name: "No External Network Calls".to_string(),
|
||||
description: "Block plans that make calls to unknown external services".to_string(),
|
||||
enabled: true,
|
||||
rules: vec![PolicyRule {
|
||||
field: "tool_calls".to_string(),
|
||||
operator: "contains".to_string(),
|
||||
value: "0.0.0.0".to_string(),
|
||||
action: "reject".to_string(),
|
||||
severity: "warning".to_string(),
|
||||
}],
|
||||
},
|
||||
Policy {
|
||||
id: "max-tool-calls".to_string(),
|
||||
name: "Maximum Tool Calls".to_string(),
|
||||
description: "Limit the number of tool calls per plan".to_string(),
|
||||
enabled: true,
|
||||
rules: vec![],
|
||||
},
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user