use lean_ctx_protocol::context_gateway::{
ClassificationV1, ContextDispositionV1, DestinationLocalityV1, ReasonCodeV1,
};
use serde_json::{Map, Value};
use super::egress::{EgressTarget, TrustedDestinationBinding, destination_with_binding};
use super::{
AdmissionPolicy, OutputCompleteness, SourceAdmission, admit, admit_source_typed,
admit_uninspectable, capture, clean, fingerprint_digest,
};
pub(crate) const NO_SOURCE_AUTHORITY: &str =
"stored output has no source authority; repeat the original authorized operation";
pub(super) const NO_SOURCE_AUTHORITY_CODE: &str = "store.no_source_authority";
pub(super) const RESTRICTED_CODE: &str = "store.restricted_content";
pub(super) const SOURCE_GONE_CODE: &str = "store.source_unavailable";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Withheld {
codes: Vec<String>,
}
impl Withheld {
pub fn new(codes: &[&str]) -> Self {
Self {
codes: codes.iter().map(|code| (*code).to_owned()).collect(),
}
}
pub(super) fn from_reasons(reasons: &[ReasonCodeV1]) -> Self {
let mut codes: Vec<String> = reasons
.iter()
.map(|code| code.as_str().to_owned())
.collect();
if codes.is_empty() {
codes.push("policy.content_withheld".to_owned());
}
Self { codes }
}
pub fn no_source_authority() -> Self {
Self::new(&[NO_SOURCE_AUTHORITY_CODE])
}
pub fn source_unavailable() -> Self {
Self::new(&[SOURCE_GONE_CODE])
}
pub fn codes(&self) -> &[String] {
&self.codes
}
#[cfg(any(test, feature = "test-support"))]
pub fn has(&self, code: &str) -> bool {
self.codes.iter().any(|candidate| candidate == code)
}
pub fn message(&self, subject: &str) -> String {
if self
.codes
.iter()
.any(|code| code.starts_with("store.") && code != RESTRICTED_CODE)
{
return format!("{NO_SOURCE_AUTHORITY} ({})", self.codes.join(", "));
}
format!(
"{subject} withheld by the context gateway ({})",
self.codes.join(", ")
)
}
pub fn notice(&self) -> String {
format!(
"[lean-ctx gateway: content withheld — {}]",
self.codes.join(", ")
)
}
}
impl std::fmt::Display for Withheld {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.message("content"))
}
}
pub(super) fn current_policy() -> AdmissionPolicy {
match capture::current()
.as_deref()
.and_then(capture::AdmissionCapture::snapshot)
{
Some(snapshot) => snapshot.admission().admission_policy().clone(),
None => AdmissionPolicy::from_config(&crate::core::config::Config::load_arc()),
}
}
pub fn gateway_snapshot() -> super::stores::StoreAdmission {
super::stores::StoreAdmission::with_policy(current_policy())
}
pub fn admit_carrier_text(text: &str) -> Result<SourceAdmission, Withheld> {
admit_carrier_text_under(text, None)
}
pub(crate) fn admit_carrier_text_under(
text: &str,
policy: Option<&AdmissionPolicy>,
) -> Result<SourceAdmission, Withheld> {
admit_source_typed(text, None, false, OutputCompleteness::complete(), policy)
.map_err(|reasons| Withheld::from_reasons(&reasons))
}
pub fn admit_for_store(text: &str) -> Result<String, Withheld> {
admit_for_store_under(text, None)
}
pub fn admit_for_store_under(
text: &str,
policy: Option<&AdmissionPolicy>,
) -> Result<String, Withheld> {
let admission = admit_carrier_text_under(text, policy)?;
if admission.storable {
Ok(admission.text)
} else {
Err(Withheld::new(&[RESTRICTED_CODE]))
}
}
fn record(policy: &AdmissionPolicy, admission: &super::Admission) {
let decision = &admission.decision;
if decision.disposition == ContextDispositionV1::Deny {
crate::core::hive_signals::admission_refused(
decision.reason_codes.iter().map(ReasonCodeV1::as_str),
);
}
if let Some(capture) = capture::current() {
capture.bind_policy(
policy.gateway.mode,
fingerprint_digest(&policy.fingerprint()),
);
capture.record(&admission.decision, admission.counts);
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Downstream<'a> {
pub server: &'a str,
pub endpoint: Option<&'a str>,
pub tool: &'a str,
}
const MAX_ARGUMENT_NODES: usize = 10_000;
const MAX_ARGUMENT_DEPTH: usize = 64;
pub(crate) fn admit_downstream_arguments(
downstream: &Downstream<'_>,
arguments: &mut Map<String, Value>,
) -> Result<(), Withheld> {
let policy = current_policy();
if let Some(codes) = policy.config_refusal_codes() {
return Err(Withheld::new(&codes));
}
let provider = format!("mcp:{}", downstream.server);
let destination = destination_with_binding(
&EgressTarget {
provider: &provider,
model: None,
upstream_base: downstream.endpoint.unwrap_or_default(),
},
TrustedDestinationBinding {
provider_id: None,
local: downstream.endpoint.is_none().then_some(true),
account_ref: None,
region: None,
},
);
if policy.gateway.has_destination_policy() && !policy.gateway.allows_destination(&destination) {
return Err(Withheld::new(&["destination.constraint_unsatisfied"]));
}
let mut walk = ArgumentWalk {
policy: &policy,
project_active: crate::core::policy::runtime::is_active(),
gateway_active: policy.enabled_effective(),
remote: destination.locality != DestinationLocalityV1::Local,
nodes: 0,
};
walk.immutable(downstream.tool)?;
for (key, value) in arguments.iter_mut() {
walk.immutable(key)?;
walk.value(value, 0)?;
}
Ok(())
}
struct ArgumentWalk<'a> {
policy: &'a AdmissionPolicy,
project_active: bool,
gateway_active: bool,
remote: bool,
nodes: usize,
}
impl ArgumentWalk<'_> {
fn bounded(&mut self, depth: usize) -> Result<bool, Withheld> {
self.nodes += 1;
if self.nodes <= MAX_ARGUMENT_NODES && depth <= MAX_ARGUMENT_DEPTH {
return Ok(true);
}
if self.policy.detectors_mandatory() || self.project_active {
return Err(Withheld::new(&["coverage.partial"]));
}
Ok(false)
}
fn value(&mut self, value: &mut Value, depth: usize) -> Result<(), Withheld> {
if !self.bounded(depth)? {
return Ok(());
}
match value {
Value::String(text) => {
if let Some(admitted) = self.admitted(text)? {
*text = admitted;
}
Ok(())
}
Value::Array(items) => items
.iter_mut()
.try_for_each(|item| self.value(item, depth + 1)),
Value::Object(map) => map.iter_mut().try_for_each(|(key, child)| {
self.immutable(key)?;
self.value(child, depth + 1)
}),
Value::Null | Value::Bool(_) | Value::Number(_) => Ok(()),
}
}
fn immutable(&mut self, text: &str) -> Result<(), Withheld> {
match self.admitted(text)? {
None => Ok(()),
Some(_) => Err(Withheld::new(&["egress.immutable_position"])),
}
}
fn admitted(&mut self, text: &str) -> Result<Option<String>, Withheld> {
if text.is_empty() || (!self.project_active && !self.gateway_active) {
return Ok(None);
}
let project_original = if self.project_active {
crate::core::policy::content::protect_active(text)
.map_err(|_| Withheld::new(&["policy.content_withheld"]))?
.into_owned()
} else {
text.to_owned()
};
if !self.gateway_active {
return Ok((project_original != text).then_some(project_original));
}
let admission = admit(text, None, self.policy);
record(self.policy, &admission);
let Some(gateway_output) = admission.text.as_deref() else {
return Err(Withheld::from_reasons(&admission.decision.reason_codes));
};
if admission.classification >= ClassificationV1::Restricted {
return Err(Withheld::new(&[if self.remote {
"destination.remote_restricted"
} else {
"egress.restricted_content"
}]));
}
let delivered = if self.project_active {
super::compose_active_gateway_output(
&project_original,
gateway_output,
self.policy,
OutputCompleteness::complete(),
)
.ok_or_else(|| Withheld::new(&["policy.content_withheld"]))?
} else {
gateway_output.to_owned()
};
Ok((delivered != text).then_some(delivered))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Release {
Unchanged,
Redacted {
text: String,
notice: String,
},
Withheld(Withheld),
}
pub struct ReleaseGate {
policy: AdmissionPolicy,
fingerprint: Vec<u8>,
}
impl ReleaseGate {
#[must_use]
pub fn current() -> Self {
Self::with(current_policy())
}
#[must_use]
pub(crate) fn under(snapshot: &super::stores::StoreAdmission) -> Self {
Self::with(snapshot.admission_policy().clone())
}
fn with(mut policy: AdmissionPolicy) -> Self {
policy.semantic = None;
let fingerprint = policy.fingerprint();
Self {
policy,
fingerprint,
}
}
pub fn inactive(&self) -> bool {
!self.policy.enabled_effective()
}
pub fn text(&self, text: &str) -> Release {
if text.is_empty() || self.inactive() {
return Release::Unchanged;
}
let key = self
.policy
.semantic
.is_none()
.then(|| clean::key(&self.fingerprint, "release", text));
if let Some(key) = &key
&& clean::get(key).is_some()
{
return Release::Unchanged;
}
let admission = admit(text, None, &self.policy);
match admission.text.as_deref() {
Some(delivered) if delivered == text => {
if let Some(key) = key
&& admission.decision.reason_codes.is_empty()
&& admission.disposition() == ContextDispositionV1::Allow
{
clean::insert(key, admission.decision.clone());
}
Release::Unchanged
}
Some(delivered) => {
record(&self.policy, &admission);
Release::Redacted {
text: delivered.to_owned(),
notice: admission
.hud_line()
.unwrap_or_else(|| "[lean-ctx gateway: content redacted]".to_owned()),
}
}
None => {
record(&self.policy, &admission);
Release::Withheld(Withheld::from_reasons(&admission.decision.reason_codes))
}
}
}
pub fn media(&self, bytes: u64) -> Result<(), Withheld> {
if self.inactive() {
return Ok(());
}
let admission = admit_uninspectable(None, bytes, &self.policy);
if admission.text.is_some() {
return Ok(());
}
record(&self.policy, &admission);
Err(Withheld::from_reasons(&admission.decision.reason_codes))
}
}
#[cfg(test)]
mod tests;