use crate::state::AppState;
use aingle_graph::{NodeId, Predicate, Triple, Value};
use axum::{
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Debug)]
pub struct ValidateManifestRequest {
pub assertions: Vec<AssertionDecl>,
pub namespace: String,
}
#[derive(Deserialize, Debug)]
pub struct AssertionDecl {
pub predicate: String,
#[serde(default)]
pub require_proof: bool,
}
#[derive(Serialize, Debug)]
pub struct ValidateManifestResponse {
pub valid: bool,
pub errors: Vec<String>,
}
#[derive(Deserialize, Debug)]
pub struct CreateSandboxRequest {
pub namespace: String,
#[serde(default = "default_ttl")]
pub ttl_seconds: u64,
}
fn default_ttl() -> u64 {
300
}
#[derive(Serialize, Debug)]
pub struct CreateSandboxResponse {
pub id: String,
pub namespace: String,
}
pub async fn validate_manifest(
State(state): State<AppState>,
Json(req): Json<ValidateManifestRequest>,
) -> impl IntoResponse {
let logic = state.logic.read().await;
let mut errors: Vec<String> = Vec::new();
for assertion in &req.assertions {
let ns_pred = if assertion.predicate.contains(':') {
assertion.predicate.clone()
} else {
format!("{}:{}", req.namespace, assertion.predicate)
};
if assertion.require_proof {
let test_triple = Triple::new(
NodeId::named(format!("{}:_test", req.namespace)),
Predicate::named(&ns_pred),
Value::literal("_test_value"),
);
let result = logic.validate(&test_triple);
if result.matches.is_empty() {
errors.push(format!(
"Assertion predicate '{}' requires proof but no PoL rules found",
ns_pred
));
}
}
}
let valid = errors.is_empty();
Json(ValidateManifestResponse { valid, errors })
}
pub async fn create_sandbox(
State(state): State<AppState>,
Json(req): Json<CreateSandboxRequest>,
) -> impl IntoResponse {
let sandbox_id = format!("sandbox-{}", uuid::Uuid::new_v4());
let sandbox_ns = format!("{}:{}", req.namespace, sandbox_id);
state
.sandbox_manager
.create(sandbox_id.clone(), sandbox_ns.clone(), req.ttl_seconds)
.await;
(
StatusCode::CREATED,
Json(CreateSandboxResponse {
id: sandbox_id,
namespace: sandbox_ns,
}),
)
}
pub async fn delete_sandbox(
State(state): State<AppState>,
Path(sandbox_id): Path<String>,
) -> impl IntoResponse {
let removed = state.sandbox_manager.remove(&sandbox_id).await;
if let Some(namespace) = removed {
let graph = state.graph.write().await;
let deleted = graph.delete_by_subject_prefix(&namespace).unwrap_or(0);
Json(serde_json::json!({
"deleted": true,
"namespace": namespace,
"triples_removed": deleted
}))
} else {
Json(serde_json::json!({
"deleted": false,
"error": "sandbox not found"
}))
}
}
pub fn skill_verification_router() -> axum::Router<AppState> {
axum::Router::new()
.route(
"/api/v1/skills/validate",
axum::routing::post(validate_manifest),
)
.route(
"/api/v1/skills/sandbox",
axum::routing::post(create_sandbox),
)
.route(
"/api/v1/skills/sandbox/{id}",
axum::routing::delete(delete_sandbox),
)
}