use crate::{
AnnotatorDispatcher, AnnotatorInvocation, Decision, EnforcementMode, InterventionPoint,
InterventionPointRequest, JsonValue, Manifest, PerfTelemetry, PolicyDispatcher,
PreparedPolicyInvocation, Runtime, RuntimeError, Verdict,
};
use serde_json::json;
use std::{
ffi::{CStr, CString},
os::raw::{c_char, c_void},
panic::{catch_unwind, AssertUnwindSafe},
path::Path,
str::FromStr,
sync::Arc,
};
macro_rules! ffi_guard {
(ptr_with_err, $err:expr, $body:block) => {{
match catch_unwind(AssertUnwindSafe(|| $body)) {
Ok(value) => value,
Err(payload) => {
let msg = panic_message(&payload);
unsafe { write_err($err, &format!("panic crossing ACS FFI boundary: {msg}")) };
std::ptr::null_mut()
}
}
}};
(code_with_err, $err:expr, $sentinel:expr, $body:block) => {{
match catch_unwind(AssertUnwindSafe(|| $body)) {
Ok(value) => value,
Err(payload) => {
let msg = panic_message(&payload);
unsafe { write_err($err, &format!("panic crossing ACS FFI boundary: {msg}")) };
$sentinel
}
}
}};
(void, $body:block) => {{
let _ = catch_unwind(AssertUnwindSafe(|| $body));
}};
}
pub struct AcsBuilder {
manifest: Option<Manifest>,
annotations: Option<Arc<dyn AnnotatorDispatcher>>,
policy: Option<Arc<dyn PolicyDispatcher>>,
perf_telemetry: PerfTelemetry,
enable_default_annotations: bool,
enable_default_policy: bool,
}
pub struct AcsRuntime {
runtime: Runtime,
}
pub type AcsFreeResultCallback = unsafe extern "C" fn(ptr: *mut c_char, user_data: *mut c_void);
pub type AcsAnnotatorCallback = unsafe extern "C" fn(
annotator_name: *const c_char,
annotator_json: *const c_char,
preliminary_policy_input_json: *const c_char,
user_data: *mut c_void,
) -> *mut c_char;
pub type AcsPolicyCallback = unsafe extern "C" fn(
prepared_invocation_json: *const c_char,
user_data: *mut c_void,
) -> *mut c_char;
struct CallbackHolder<F> {
cb: F,
free: AcsFreeResultCallback,
user_data: *mut c_void,
}
unsafe impl<F> Send for CallbackHolder<F> {}
unsafe impl<F> Sync for CallbackHolder<F> {}
impl<F> CallbackHolder<F> {
unsafe fn read_and_free(&self, ptr: *mut c_char) -> Result<String, &'static str> {
if ptr.is_null() {
return Err("callback returned null");
}
let result = unsafe { CStr::from_ptr(ptr) }.to_str().map(str::to_owned);
unsafe { (self.free)(ptr, self.user_data) };
result.map_err(|_| "callback returned non-UTF8 string")
}
}
struct CAnnotatorDispatcher {
holder: Arc<CallbackHolder<AcsAnnotatorCallback>>,
}
impl AnnotatorDispatcher for CAnnotatorDispatcher {
fn dispatch(
&self,
annotator_name: &str,
annotator: &AnnotatorInvocation,
preliminary_policy_input: &JsonValue,
) -> Result<JsonValue, RuntimeError> {
let annotator_json = serde_json::to_string(annotator)
.map_err(|err| RuntimeError::AnnotationFailed(err.to_string()))?;
let preliminary_json = serde_json::to_string(preliminary_policy_input)
.map_err(|err| RuntimeError::AnnotationFailed(err.to_string()))?;
let annotator_name_c = cstring_lossy(annotator_name);
let annotator_json_c = cstring_lossy(&annotator_json);
let preliminary_json_c = cstring_lossy(&preliminary_json);
let raw = unsafe {
(self.holder.cb)(
annotator_name_c.as_ptr(),
annotator_json_c.as_ptr(),
preliminary_json_c.as_ptr(),
self.holder.user_data,
)
};
let returned = unsafe { self.holder.read_and_free(raw) }
.map_err(|err| RuntimeError::AnnotationFailed(err.to_string()))?;
if returned == crate::reserved_reason::ANNOTATION_TIMEOUT {
return Err(RuntimeError::AnnotationTimeout(returned));
}
if returned == crate::reserved_reason::ANNOTATION_FAILED {
return Err(RuntimeError::AnnotationFailed(returned));
}
serde_json::from_str(&returned)
.map_err(|err| RuntimeError::AnnotationFailed(err.to_string()))
}
}
struct CPolicyDispatcher {
holder: Arc<CallbackHolder<AcsPolicyCallback>>,
}
impl PolicyDispatcher for CPolicyDispatcher {
fn evaluate(&self, invocation: &PreparedPolicyInvocation) -> Result<JsonValue, RuntimeError> {
let invocation_json = serde_json::to_string(invocation)
.map_err(|err| RuntimeError::PolicyInvocationFailed(err.to_string()))?;
let invocation_json_c = cstring_lossy(&invocation_json);
let raw = unsafe { (self.holder.cb)(invocation_json_c.as_ptr(), self.holder.user_data) };
let returned = unsafe { self.holder.read_and_free(raw) }
.map_err(|err| RuntimeError::PolicyInvocationFailed(err.to_string()))?;
serde_json::from_str(&returned)
.map_err(|err| RuntimeError::PolicyInvocationFailed(err.to_string()))
}
}
fn builder_from_manifest(manifest: Manifest) -> *mut AcsBuilder {
Box::into_raw(Box::new(AcsBuilder {
manifest: Some(manifest),
annotations: None,
policy: None,
perf_telemetry: PerfTelemetry::default(),
enable_default_annotations: false,
enable_default_policy: false,
}))
}
#[no_mangle]
pub unsafe extern "C" fn acs_builder_from_path(
path: *const c_char,
err: *mut *mut c_char,
) -> *mut AcsBuilder {
ffi_guard!(ptr_with_err, err, {
let path = match unsafe { cstr_to_str(path) } {
Some(value) => value,
None => {
unsafe { write_err(err, "null or non-UTF8 path") };
return std::ptr::null_mut();
}
};
match Manifest::from_path(Path::new(path)) {
Ok(manifest) => builder_from_manifest(manifest),
Err(error) => {
unsafe { write_err(err, &format!("from_path failed: {error}")) };
std::ptr::null_mut()
}
}
})
}
#[no_mangle]
pub unsafe extern "C" fn acs_builder_from_yaml_chain(
yamls: *const *const c_char,
n: usize,
err: *mut *mut c_char,
) -> *mut AcsBuilder {
ffi_guard!(ptr_with_err, err, {
if yamls.is_null() {
unsafe { write_err(err, "null yamls array") };
return std::ptr::null_mut();
}
if n == 0 {
unsafe { write_err(err, "empty yamls array") };
return std::ptr::null_mut();
}
let mut owned: Vec<&str> = Vec::with_capacity(n);
for index in 0..n {
let entry = unsafe { *yamls.add(index) };
let yaml = match unsafe { cstr_to_str(entry) } {
Some(value) => value,
None => {
unsafe { write_err(err, &format!("yamls[{index}] is null or non-UTF8")) };
return std::ptr::null_mut();
}
};
owned.push(yaml);
}
match Manifest::from_yaml_chain(&owned) {
Ok(manifest) => builder_from_manifest(manifest),
Err(error) => {
unsafe { write_err(err, &format!("from_yaml_chain failed: {error}")) };
std::ptr::null_mut()
}
}
})
}
#[no_mangle]
pub unsafe extern "C" fn acs_builder_from_yaml(
yaml: *const c_char,
err: *mut *mut c_char,
) -> *mut AcsBuilder {
ffi_guard!(ptr_with_err, err, {
let yaml = match unsafe { cstr_to_str(yaml) } {
Some(value) => value,
None => {
unsafe { write_err(err, "null or non-UTF8 yaml") };
return std::ptr::null_mut();
}
};
match Manifest::from_yaml_str(yaml) {
Ok(manifest) => builder_from_manifest(manifest),
Err(error) => {
unsafe { write_err(err, &format!("from_yaml failed: {error}")) };
std::ptr::null_mut()
}
}
})
}
#[no_mangle]
pub unsafe extern "C" fn acs_builder_from_json(
json: *const c_char,
err: *mut *mut c_char,
) -> *mut AcsBuilder {
ffi_guard!(ptr_with_err, err, {
let json = match unsafe { cstr_to_str(json) } {
Some(value) => value,
None => {
unsafe { write_err(err, "null or non-UTF8 json") };
return std::ptr::null_mut();
}
};
match Manifest::from_json_str(json) {
Ok(manifest) => builder_from_manifest(manifest),
Err(error) => {
unsafe { write_err(err, &format!("from_json failed: {error}")) };
std::ptr::null_mut()
}
}
})
}
#[no_mangle]
pub unsafe extern "C" fn acs_builder_register_annotator_dispatcher(
b: *mut AcsBuilder,
cb: Option<AcsAnnotatorCallback>,
free_result: Option<AcsFreeResultCallback>,
user_data: *mut c_void,
err: *mut *mut c_char,
) -> i32 {
ffi_guard!(code_with_err, err, -1, {
let Some(cb) = require_callback(cb, err, "annotator_dispatcher") else {
return -1;
};
let Some(free) = require_callback(free_result, err, "free_result") else {
return -1;
};
let Some(builder) = (unsafe { b.as_mut() }) else {
unsafe { write_err(err, "null builder") };
return -1;
};
builder.annotations = Some(Arc::new(CAnnotatorDispatcher {
holder: Arc::new(CallbackHolder {
cb,
free,
user_data,
}),
}));
0
})
}
#[no_mangle]
pub unsafe extern "C" fn acs_builder_register_policy_dispatcher(
b: *mut AcsBuilder,
cb: Option<AcsPolicyCallback>,
free_result: Option<AcsFreeResultCallback>,
user_data: *mut c_void,
err: *mut *mut c_char,
) -> i32 {
ffi_guard!(code_with_err, err, -1, {
let Some(cb) = require_callback(cb, err, "policy_dispatcher") else {
return -1;
};
let Some(free) = require_callback(free_result, err, "free_result") else {
return -1;
};
let Some(builder) = (unsafe { b.as_mut() }) else {
unsafe { write_err(err, "null builder") };
return -1;
};
builder.policy = Some(Arc::new(CPolicyDispatcher {
holder: Arc::new(CallbackHolder {
cb,
free,
user_data,
}),
}));
0
})
}
#[cfg(feature = "default-dispatchers")]
#[no_mangle]
pub unsafe extern "C" fn acs_builder_enable_default_annotator_dispatcher(
b: *mut AcsBuilder,
err: *mut *mut c_char,
) -> i32 {
ffi_guard!(code_with_err, err, -1, {
let Some(builder) = (unsafe { b.as_mut() }) else {
unsafe { write_err(err, "null builder") };
return -1;
};
builder.enable_default_annotations = true;
0
})
}
#[cfg(feature = "default-dispatchers")]
#[no_mangle]
pub unsafe extern "C" fn acs_builder_enable_default_policy_dispatcher(
b: *mut AcsBuilder,
err: *mut *mut c_char,
) -> i32 {
ffi_guard!(code_with_err, err, -1, {
let Some(builder) = (unsafe { b.as_mut() }) else {
unsafe { write_err(err, "null builder") };
return -1;
};
builder.enable_default_policy = true;
0
})
}
#[no_mangle]
pub unsafe extern "C" fn acs_builder_set_perf_telemetry(
b: *mut AcsBuilder,
level: i32,
err: *mut *mut c_char,
) -> i32 {
ffi_guard!(code_with_err, err, -1, {
let Some(builder) = (unsafe { b.as_mut() }) else {
unsafe { write_err(err, "null builder") };
return -1;
};
if !(0..=2).contains(&level) {
unsafe { write_err(err, "perf telemetry level must be 0, 1, or 2") };
return -1;
}
let perf_telemetry = PerfTelemetry::from_u8(level as u8)
.expect("range check ensures valid perf telemetry level");
builder.perf_telemetry = perf_telemetry;
0
})
}
fn resolve_default_annotator_dispatcher(
builder: &AcsBuilder,
_manifest: &Manifest,
) -> Result<Option<Arc<dyn AnnotatorDispatcher>>, String> {
if !builder.enable_default_annotations {
return Ok(None);
}
#[cfg(feature = "default-dispatchers")]
{
let _ = _manifest;
Ok(Some(crate::dispatchers::default_annotator_dispatcher()))
}
#[cfg(not(feature = "default-dispatchers"))]
{
Err("default annotator dispatcher is unavailable; rebuild with the 'default-dispatchers' feature".to_string())
}
}
fn resolve_default_policy_dispatcher(
builder: &AcsBuilder,
manifest: &Manifest,
) -> Result<Option<Arc<dyn PolicyDispatcher>>, String> {
if !builder.enable_default_policy {
return Ok(None);
}
#[cfg(all(feature = "default-dispatchers", feature = "opa"))]
{
crate::dispatchers::default_policy_dispatcher(manifest)
.map(Some)
.map_err(|error| error.to_string())
}
#[cfg(not(all(feature = "default-dispatchers", feature = "opa")))]
{
let _ = manifest;
Err("default policy dispatcher is unavailable; rebuild with the 'default-dispatchers' and 'opa' features".to_string())
}
}
#[no_mangle]
pub unsafe extern "C" fn acs_builder_build(
b: *mut AcsBuilder,
err: *mut *mut c_char,
) -> *mut AcsRuntime {
ffi_guard!(ptr_with_err, err, {
if b.is_null() {
unsafe { write_err(err, "null builder") };
return std::ptr::null_mut();
}
let mut builder = unsafe { Box::from_raw(b) };
let manifest = match builder.manifest.take() {
Some(manifest) => manifest,
None => {
unsafe { write_err(err, "builder already consumed") };
return std::ptr::null_mut();
}
};
let annotations = match builder.annotations.take() {
Some(dispatcher) => dispatcher,
None => match resolve_default_annotator_dispatcher(&builder, &manifest) {
Ok(Some(dispatcher)) => dispatcher,
Ok(None) => {
unsafe { write_err(err, "annotator dispatcher not registered") };
return std::ptr::null_mut();
}
Err(message) => {
unsafe { write_err(err, &message) };
return std::ptr::null_mut();
}
},
};
let policy = match builder.policy.take() {
Some(dispatcher) => dispatcher,
None => match resolve_default_policy_dispatcher(&builder, &manifest) {
Ok(Some(dispatcher)) => dispatcher,
Ok(None) => {
unsafe { write_err(err, "policy dispatcher not registered") };
return std::ptr::null_mut();
}
Err(message) => {
unsafe { write_err(err, &message) };
return std::ptr::null_mut();
}
},
};
match Runtime::with_perf_telemetry(manifest, annotations, policy, builder.perf_telemetry) {
Ok(runtime) => Box::into_raw(Box::new(AcsRuntime { runtime })),
Err(error) => {
unsafe { write_err(err, &format!("build failed: {error}")) };
std::ptr::null_mut()
}
}
})
}
#[no_mangle]
pub unsafe extern "C" fn acs_builder_free(b: *mut AcsBuilder) {
ffi_guard!(void, {
if !b.is_null() {
unsafe { drop(Box::from_raw(b)) };
}
})
}
#[no_mangle]
pub unsafe extern "C" fn acs_runtime_evaluate(
r: *const AcsRuntime,
request_json: *const c_char,
err: *mut *mut c_char,
) -> *mut c_char {
ffi_guard!(ptr_with_err, err, {
let Some(runtime) = (unsafe { r.as_ref() }) else {
unsafe { write_err(err, "null runtime") };
return std::ptr::null_mut();
};
let request_str = match unsafe { cstr_to_str(request_json) } {
Some(value) => value,
None => {
unsafe { write_err(err, "null or non-UTF8 request_json") };
return std::ptr::null_mut();
}
};
let request_value: JsonValue = match serde_json::from_str(request_str) {
Ok(value) => value,
Err(_) => return request_invalid_response(),
};
let Some(object) = request_value.as_object() else {
return request_invalid_response();
};
let Some(intervention_point_str) =
object.get("intervention_point").and_then(JsonValue::as_str)
else {
return request_invalid_response();
};
let intervention_point = match InterventionPoint::from_str(intervention_point_str) {
Ok(value) => value,
Err(_) => {
let error =
RuntimeError::InterventionPointUnknown(intervention_point_str.to_string());
return json_to_c(&json!({
"verdict": Verdict::runtime_error(&error),
"transformed_policy_target": null,
"transformed_policy_target_applied": false,
"policy_input": null,
"action_identity": null,
}));
}
};
let mode = match object.get("mode") {
None => EnforcementMode::Enforce,
Some(JsonValue::String(mode_str)) => match EnforcementMode::from_str(mode_str) {
Ok(value) => value,
Err(_) => return request_invalid_response(),
},
Some(_) => return request_invalid_response(),
};
let Some(snapshot) = object.get("snapshot").cloned() else {
return request_invalid_response();
};
if !snapshot.is_object() {
return request_invalid_response();
}
let result = runtime
.runtime
.evaluate_intervention_point(InterventionPointRequest {
intervention_point,
snapshot,
mode,
});
let response = json!({
"verdict": result.verdict,
"transformed_policy_target": result.transformed_policy_target,
"transformed_policy_target_applied": result.transformed_policy_target.is_some(),
"policy_input": result.policy_input,
"action_identity": result.action_identity,
"input_identity": result.input_identity,
"enforced_identity": result.enforced_identity,
});
json_to_c(&response)
})
}
#[no_mangle]
pub unsafe extern "C" fn acs_runtime_free(r: *mut AcsRuntime) {
ffi_guard!(void, {
if !r.is_null() {
unsafe { drop(Box::from_raw(r)) };
}
})
}
#[no_mangle]
pub unsafe extern "C" fn acs_free_string(s: *mut c_char) {
ffi_guard!(void, {
if !s.is_null() {
unsafe { drop(CString::from_raw(s)) };
}
})
}
unsafe fn cstr_to_str<'a>(ptr: *const c_char) -> Option<&'a str> {
if ptr.is_null() {
return None;
}
unsafe { CStr::from_ptr(ptr) }.to_str().ok()
}
fn string_to_c(s: &str) -> *mut c_char {
match CString::new(s) {
Ok(cstring) => cstring.into_raw(),
Err(_) => std::ptr::null_mut(),
}
}
fn json_to_c(value: &JsonValue) -> *mut c_char {
match serde_json::to_string(value) {
Ok(serialized) => string_to_c(&serialized),
Err(_) => string_to_c(r#"{"error":"serialization failed"}"#),
}
}
fn request_invalid_response() -> *mut c_char {
json_to_c(&json!({
"verdict": {
"decision": Decision::Deny,
"reason": "runtime_error:request_invalid",
"message": "Request blocked by Agent Control Specification."
},
"transformed_policy_target": null,
"transformed_policy_target_applied": false,
"policy_input": null,
"action_identity": null,
}))
}
unsafe fn write_err(err: *mut *mut c_char, msg: &str) {
if !err.is_null() {
unsafe { *err = string_to_c(msg) };
}
}
fn require_callback<F>(cb: Option<F>, err_out: *mut *mut c_char, slot: &str) -> Option<F> {
match cb {
Some(callback) => Some(callback),
None => {
unsafe { write_err(err_out, &format!("required callback `{slot}` is null")) };
None
}
}
}
fn cstring_lossy(s: &str) -> CString {
let cleaned = s.replace('\0', " ");
CString::new(cleaned).unwrap_or_else(|_| CString::new("").expect("empty string has no NUL"))
}
fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
if let Some(message) = payload.downcast_ref::<&str>() {
(*message).to_string()
} else if let Some(message) = payload.downcast_ref::<String>() {
message.clone()
} else {
"unknown panic".to_string()
}
}