tandem-server 0.6.0

HTTP server for Tandem engine APIs
use serde::Serialize;
use tandem_types::RuntimeAuthMode;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MemoryPolicyMode {
    LocalGlobal,
    EnterpriseScoped,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct MemoryContextPolicyStatus {
    pub runtime_auth_mode: RuntimeAuthMode,
    pub effective_memory_auth_mode: RuntimeAuthMode,
    pub memory_policy_mode: MemoryPolicyMode,
    pub strict_required: bool,
    pub context_assertion_verifier_configured: bool,
    pub hosted_control_plane_configured: bool,
    pub cross_tenant_grant_signing_key_configured: bool,
    pub strict_memory_enforcement: bool,
    pub strict_mcp_enforcement: bool,
    pub strict_tool_enforcement: bool,
    pub startup_ready: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub failure_reason: Option<&'static str>,
}

impl MemoryContextPolicyStatus {
    pub fn from_parts(
        runtime_auth_mode: RuntimeAuthMode,
        context_assertion_verifier_configured: bool,
        hosted_control_plane_configured: bool,
        cross_tenant_grant_signing_key_configured: bool,
        strict_memory_enforcement: bool,
        strict_mcp_enforcement: bool,
        strict_tool_enforcement: bool,
    ) -> Self {
        let strict_required = enterprise_memory_policy_required(
            runtime_auth_mode,
            context_assertion_verifier_configured,
            hosted_control_plane_configured,
        );
        let strict_active =
            strict_memory_enforcement && strict_mcp_enforcement && strict_tool_enforcement;
        let failure_reason = if strict_required && !strict_active {
            Some("strict_tenant_enforcement_unavailable")
        } else {
            None
        };
        let effective_memory_auth_mode =
            if strict_required && runtime_auth_mode == RuntimeAuthMode::LocalSingleTenant {
                RuntimeAuthMode::EnterpriseRequired
            } else {
                runtime_auth_mode
            };
        Self {
            runtime_auth_mode,
            effective_memory_auth_mode,
            memory_policy_mode: if strict_required {
                MemoryPolicyMode::EnterpriseScoped
            } else {
                MemoryPolicyMode::LocalGlobal
            },
            strict_required,
            context_assertion_verifier_configured,
            hosted_control_plane_configured,
            cross_tenant_grant_signing_key_configured,
            strict_memory_enforcement,
            strict_mcp_enforcement,
            strict_tool_enforcement,
            startup_ready: failure_reason.is_none(),
            failure_reason,
        }
    }

    pub fn ensure_startup_ready(&self) -> anyhow::Result<()> {
        if let Some(reason) = self.failure_reason {
            anyhow::bail!(
                "enterprise memory/context policy is not ready: {reason}; runtime_auth_mode={}, memory_policy_mode={:?}, strict_memory={}, strict_mcp={}, strict_tools={}",
                self.runtime_auth_mode.as_str(),
                self.memory_policy_mode,
                self.strict_memory_enforcement,
                self.strict_mcp_enforcement,
                self.strict_tool_enforcement
            );
        }
        Ok(())
    }
}

pub fn enterprise_memory_policy_required(
    runtime_auth_mode: RuntimeAuthMode,
    context_assertion_verifier_configured: bool,
    hosted_control_plane_configured: bool,
) -> bool {
    runtime_auth_mode != RuntimeAuthMode::LocalSingleTenant
        || context_assertion_verifier_configured
        || hosted_control_plane_configured
}

pub fn resolve_memory_context_runtime_auth_mode() -> RuntimeAuthMode {
    let runtime_auth_mode = crate::config::env::resolve_runtime_auth_mode();
    if enterprise_memory_policy_required(
        runtime_auth_mode,
        crate::config::env::context_assertion_verifier_configured(),
        crate::config::env::hosted_control_plane_configured(),
    ) {
        match runtime_auth_mode {
            RuntimeAuthMode::LocalSingleTenant => RuntimeAuthMode::EnterpriseRequired,
            mode => mode,
        }
    } else {
        runtime_auth_mode
    }
}

pub fn current_memory_context_policy_status() -> MemoryContextPolicyStatus {
    MemoryContextPolicyStatus::from_parts(
        crate::config::env::resolve_runtime_auth_mode(),
        crate::config::env::context_assertion_verifier_configured(),
        crate::config::env::hosted_control_plane_configured(),
        crate::config::env::cross_tenant_grant_signing_key_configured(),
        tandem_memory::db::strict_tenant_enforcement_default(),
        tandem_runtime::mcp::strict_tenant_enforcement_default(),
        tandem_tools::strict_tenant_enforcement_default(),
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn enterprise_configured_with_strict_flag_disabled_fails_closed() {
        let status = MemoryContextPolicyStatus::from_parts(
            RuntimeAuthMode::EnterpriseRequired,
            true,
            false,
            false,
            false,
            true,
            true,
        );

        assert!(!status.startup_ready);
        assert_eq!(
            status.failure_reason,
            Some("strict_tenant_enforcement_unavailable")
        );
        assert!(status.ensure_startup_ready().is_err());
    }

    #[test]
    fn verifier_key_configured_requires_enterprise_scoped_memory() {
        let status = MemoryContextPolicyStatus::from_parts(
            RuntimeAuthMode::LocalSingleTenant,
            true,
            false,
            false,
            true,
            true,
            true,
        );

        assert!(status.startup_ready);
        assert!(status.strict_required);
        assert_eq!(
            status.memory_policy_mode,
            MemoryPolicyMode::EnterpriseScoped
        );
        assert_eq!(
            status.effective_memory_auth_mode,
            RuntimeAuthMode::EnterpriseRequired
        );
    }

    #[test]
    fn local_configured_without_signing_key_keeps_local_global_memory() {
        let status = MemoryContextPolicyStatus::from_parts(
            RuntimeAuthMode::LocalSingleTenant,
            false,
            false,
            false,
            false,
            false,
            false,
        );

        assert!(status.startup_ready);
        assert!(!status.strict_required);
        assert_eq!(status.memory_policy_mode, MemoryPolicyMode::LocalGlobal);
        assert_eq!(
            status.effective_memory_auth_mode,
            RuntimeAuthMode::LocalSingleTenant
        );
    }

    #[test]
    fn prompt_context_provider_path_keeps_legacy_global_search_local_only() {
        let source = include_str!("../app/state/prompt_context_hook.rs");
        assert!(
            source.contains(
                "crate::memory::policy_status::resolve_memory_context_runtime_auth_mode()"
            ),
            "prompt memory access must resolve the enterprise-aware memory policy mode"
        );

        let function_source = source
            .split("pub(super) async fn search_prompt_global_memory")
            .nth(1)
            .and_then(|tail| tail.split("fn extract_run_client_id").next())
            .expect("prompt global memory search helper");
        let local_arm = function_source
            .split("PromptMemoryAccess::Local { user_id, .. } =>")
            .nth(1)
            .and_then(|tail| tail.split("PromptMemoryAccess::Governed {").next())
            .expect("local prompt memory arm");
        let governed_and_blocked_arms = function_source
            .split("PromptMemoryAccess::Governed {")
            .nth(1)
            .expect("governed prompt memory arm");

        assert_eq!(local_arm.matches(".search_global_memory(").count(), 2);
        assert_eq!(
            governed_and_blocked_arms
                .matches(".search_global_memory(")
                .count(),
            0,
            "provider-bound governed prompt context must use tenant-scoped memory search"
        );
        assert!(
            governed_and_blocked_arms.contains(".search_global_memory_for_tenant("),
            "governed prompt context must use tenant-scoped memory search"
        );
    }

    #[test]
    fn enterprise_aware_policy_is_used_by_ingress_and_live_registries() {
        let middleware = include_str!("../http/middleware.rs");
        assert!(
            middleware.contains("resolve_memory_context_runtime_auth_mode()"),
            "HTTP ingress must verify assertions when verifier/control-plane config promotes local mode"
        );

        let mcp_bootstrap = include_str!("../http/mcp.rs");
        assert!(
            mcp_bootstrap.contains("current_memory_context_policy_status()"),
            "live MCP/tool registries must use the same strict policy as startup/readiness"
        );
        assert!(
            mcp_bootstrap.contains("memory_context_policy.strict_required"),
            "live MCP/tool registries must flip strict mode for config-promoted enterprise policy"
        );
    }
}