use std::sync::atomic::{AtomicBool, Ordering};
use super::types::{CaptureDecision, CaptureResult, ObservabilityStore};
static NOOP_REDACTOR_WARNED: AtomicBool = AtomicBool::new(false);
static ENABLED: AtomicBool = AtomicBool::new(false);
pub fn set_enabled(enabled: bool) {
ENABLED.store(enabled, Ordering::Release);
}
pub fn set_payload_capture_enabled(enabled: bool) {
set_enabled(enabled);
}
#[must_use]
pub fn is_enabled() -> bool {
ENABLED.load(Ordering::Acquire)
}
#[must_use]
pub fn is_payload_capture_enabled() -> bool {
is_enabled()
}
pub(crate) fn gate(store: &dyn ObservabilityStore, result: CaptureResult) -> CaptureResult {
if is_enabled() && store.acknowledge_pii_redaction() {
if store.redactor().is_noop() {
warn_attesting_store_has_noop_redactor();
}
return result;
}
CaptureResult {
system_instructions: downgrade(result.system_instructions),
input_messages: downgrade(result.input_messages),
output_messages: downgrade(result.output_messages),
}
}
fn warn_attesting_store_has_noop_redactor() {
if NOOP_REDACTOR_WARNED
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
log::warn!(
"observability store attested PII redaction (acknowledge_pii_redaction = true) but \
installs the default noop PayloadRedactor; its inline payloads may carry unredacted \
PII onto spans. Install a non-noop PayloadRedactor, or ensure the store redacts \
internally before attesting."
);
}
}
fn downgrade(decision: CaptureDecision) -> CaptureDecision {
match decision {
CaptureDecision::Reference(r) => CaptureDecision::Reference(r),
CaptureDecision::Inline | CaptureDecision::Omit => CaptureDecision::Omit,
}
}
#[cfg(test)]
mod tests {
use super::super::payload::PayloadRedactor;
use super::super::types::{CaptureDecision, CaptureResult, ObservabilityStore, PayloadBundle};
use super::*;
use agent_sdk_foundation::privacy::BaselineDetector;
use async_trait::async_trait;
use std::sync::{Arc, Mutex};
static GATE_LOCK: Mutex<()> = Mutex::new(());
fn lock_gate() -> std::sync::MutexGuard<'static, ()> {
match GATE_LOCK.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
}
}
struct GateGuard {
previous: bool,
}
impl GateGuard {
fn enable() -> Self {
let previous = is_enabled();
set_enabled(true);
Self { previous }
}
fn disable() -> Self {
let previous = is_enabled();
set_enabled(false);
Self { previous }
}
}
impl Drop for GateGuard {
fn drop(&mut self) {
set_enabled(self.previous);
}
}
struct DefaultStore;
#[async_trait]
impl ObservabilityStore for DefaultStore {
async fn capture(&self, _bundle: &PayloadBundle) -> anyhow::Result<CaptureResult> {
Ok(CaptureResult {
system_instructions: CaptureDecision::Inline,
input_messages: CaptureDecision::Inline,
output_messages: CaptureDecision::Inline,
})
}
}
struct AttestingStore {
redactor: PayloadRedactor,
}
impl AttestingStore {
fn with_real_redactor() -> anyhow::Result<Self> {
Ok(Self {
redactor: PayloadRedactor::new(Arc::new(BaselineDetector::new()?)),
})
}
fn with_noop_redactor() -> Self {
Self {
redactor: PayloadRedactor::noop(),
}
}
}
#[async_trait]
impl ObservabilityStore for AttestingStore {
async fn capture(&self, _bundle: &PayloadBundle) -> anyhow::Result<CaptureResult> {
Ok(inline_result())
}
fn redactor(&self) -> &PayloadRedactor {
&self.redactor
}
fn acknowledge_pii_redaction(&self) -> bool {
true
}
}
fn inline_result() -> CaptureResult {
CaptureResult {
system_instructions: CaptureDecision::Inline,
input_messages: CaptureDecision::Inline,
output_messages: CaptureDecision::Inline,
}
}
fn assert_all_omit(result: &CaptureResult) {
assert!(matches!(result.system_instructions, CaptureDecision::Omit));
assert!(matches!(result.input_messages, CaptureDecision::Omit));
assert!(matches!(result.output_messages, CaptureDecision::Omit));
}
fn assert_all_inline(result: &CaptureResult) {
assert!(matches!(
result.system_instructions,
CaptureDecision::Inline
));
assert!(matches!(result.input_messages, CaptureDecision::Inline));
assert!(matches!(result.output_messages, CaptureDecision::Inline));
}
#[test]
fn default_store_with_gate_open_still_omits_inline() {
let _g = lock_gate();
let _gate = GateGuard::enable();
let result = gate(&DefaultStore, inline_result());
assert_all_omit(&result);
}
#[test]
fn default_store_with_gate_closed_omits_inline() {
let _g = lock_gate();
let _gate = GateGuard::disable();
let result = gate(&DefaultStore, inline_result());
assert_all_omit(&result);
}
#[test]
fn attesting_store_with_gate_closed_still_omits_inline() {
let _g = lock_gate();
let _gate = GateGuard::disable();
let result = gate(&AttestingStore::with_noop_redactor(), inline_result());
assert_all_omit(&result);
}
#[test]
fn attesting_store_with_real_redactor_and_gate_open_passes_inline_through() -> anyhow::Result<()>
{
let _g = lock_gate();
let _gate = GateGuard::enable();
let store = AttestingStore::with_real_redactor()?;
let result = gate(&store, inline_result());
assert_all_inline(&result);
Ok(())
}
#[test]
fn attesting_store_with_noop_redactor_passes_inline_but_is_a_warned_misconfig() {
let _g = lock_gate();
let _gate = GateGuard::enable();
let result = gate(&AttestingStore::with_noop_redactor(), inline_result());
assert_all_inline(&result);
}
#[test]
fn references_pass_through_when_gate_is_closed() {
let _g = lock_gate();
let _gate = GateGuard::disable();
let input = CaptureResult {
system_instructions: CaptureDecision::Reference("sys-1".into()),
input_messages: CaptureDecision::Reference("in-1".into()),
output_messages: CaptureDecision::Omit,
};
let result = gate(&DefaultStore, input);
assert!(matches!(
result.system_instructions,
CaptureDecision::Reference(ref r) if r == "sys-1"
));
assert!(matches!(
result.input_messages,
CaptureDecision::Reference(ref r) if r == "in-1"
));
assert!(matches!(result.output_messages, CaptureDecision::Omit));
}
}