tandem-server 0.7.0

HTTP server for Tandem engine APIs
// Copyright (c) 2026 Frumu LTD
// Licensed under the Business Source License 1.1

struct ActionGateLinks {
    run_id: Option<String>,
    automation_id: Option<String>,
    node_id: Option<String>,
}

async fn action_gate_runtime_links(state: &AppState, session_id: &str) -> ActionGateLinks {
    if let Some((run_id, node_id)) = state
        .automation_v2_session_run_and_node(session_id)
        .await
    {
        let automation_id = state
            .automation_v2_runs
            .read()
            .await
            .get(&run_id)
            .map(|run| run.automation_id.clone());
        return ActionGateLinks {
            run_id: Some(run_id),
            automation_id,
            node_id,
        };
    }
    if let Some(policy) = state.routine_session_policy(session_id).await {
        return ActionGateLinks {
            run_id: Some(policy.run_id),
            automation_id: None,
            node_id: None,
        };
    }
    ActionGateLinks {
        run_id: state
            .agent_teams
            .instance_for_session(session_id)
            .await
            .and_then(|instance| instance.run_id),
        automation_id: None,
        node_id: None,
    }
}

async fn link_action_gate_policy_decision(
    state: &AppState,
    policy_decision_id: Option<&str>,
    ctx: &ToolPolicyContext,
    links: &ActionGateLinks,
    risk_tier: ToolRiskTier,
    action_binding: Option<&str>,
    approval_id: Option<&str>,
) -> anyhow::Result<()> {
    let policy_decision_id = policy_decision_id
        .ok_or_else(|| anyhow::anyhow!("action-gate policy decision was not persisted"))?;
    let mut record = state
        .get_policy_decision(policy_decision_id)
        .await
        .ok_or_else(|| anyhow::anyhow!("action-gate policy decision not found"))?;
    record.session_id = Some(ctx.session_id.clone());
    record.message_id = Some(ctx.message_id.clone());
    record.run_id.clone_from(&links.run_id);
    record.automation_id.clone_from(&links.automation_id);
    record.node_id.clone_from(&links.node_id);
    record.approval_id = approval_id.map(str::to_string);
    record.metadata["action_gate"] = json!({
        "action_binding": action_binding,
        "policy_id": "approval_gate_matrix",
        "tool": ctx.tool,
        "risk_tier": risk_tier.as_str(),
        "approval_id": approval_id,
    });
    state.record_policy_decision(record).await?;
    Ok(())
}

/// Resolve protected tools against the approval matrix and durable governance
/// approval bridge. An approved receipt is consumed before this returns allow.
pub(crate) async fn evaluate_action_gate_tool_policy(
    state: &AppState,
    ctx: &ToolPolicyContext,
    tool: &str,
) -> Option<ToolPolicyDecision> {
    let descriptor = registered_tool_security_descriptor(state, tool)
        .await
        .unwrap_or_default();
    let risk_tier = tool_risk_tier_from_name_and_descriptor(tool, &descriptor);
    if !risk_tier.approval_required_by_default() {
        return None;
    }

    let tenant_context = ctx.tenant_context.clone().unwrap_or_default();
    let actor_id = tenant_context.actor_id.clone();
    let (outcome, policy_decision_id) = state
        .enforce_action_gate(
            &tenant_context,
            &GateRequest::new(Some(risk_tier), None),
            Some(tool.to_string()),
            actor_id.clone(),
            crate::now_ms(),
        )
        .await;
    if outcome.is_allowed() {
        return None;
    }

    let links = action_gate_runtime_links(state, &ctx.session_id).await;
    let connector_generations =
        governed_connector_generation_bindings(state, tool, &tenant_context).await;
    let (action_binding, mut approval_error) =
        match governed_exact_action_binding(tool, &ctx.args, &tenant_context, &connector_generations)
        {
            Ok(binding) => (Some(binding), None),
            Err(error) => (
                None,
                Some(format!(
                    "failed to derive deployment-scoped action binding: {error}"
                )),
            ),
        };
    let mut approval_id = None;
    let mut approval_state =
        crate::app::state::governance_action_gate::ActionGateApprovalState::Denied;

    if matches!(outcome.effect, PolicyDecisionEffect::ApprovalRequired)
        && state.premium_governance_enabled()
        && approval_error.is_none()
    {
        let action_binding = action_binding
            .as_ref()
            .expect("approval binding is present when binding derivation succeeded");
        let request = state
            .request_approval(
                crate::automation_v2::governance::GovernanceApprovalRequestType::ElevatedCapability,
                crate::automation_v2::governance::GovernanceActorRef::agent(
                    actor_id,
                    "action_gate_tool_policy",
                ),
                crate::automation_v2::governance::GovernanceResourceRef {
                    resource_type: "protected_action".to_string(),
                    id: action_binding.clone(),
                },
                format!("Review protected tool action `{tool}`"),
                json!({
                    "action_gate": {
                        "action_hash": action_binding,
                        "session_id": ctx.session_id,
                        "message_id": ctx.message_id,
                        "run_id": links.run_id,
                        "automation_id": links.automation_id,
                        "node_id": links.node_id,
                        "tool": tool,
                        "risk_tier": risk_tier.as_str(),
                        "policy_id": "approval_gate_matrix",
                        "policy_decision_id": policy_decision_id,
                    }
                }),
                Some(crate::now_ms().saturating_add(outcome.approval_ttl_ms)),
                &tenant_context,
            )
            .await;
        match request {
            Ok(request) => {
                approval_id = Some(request.approval_id.clone());
                match state
                    .consume_action_gate_approval(&request.approval_id, &tenant_context)
                    .await
                {
                    Ok(state) => approval_state = state,
                    Err(error) => approval_error = Some(error.to_string()),
                }
            }
            Err(error) => approval_error = Some(error.to_string()),
        }
    } else if matches!(outcome.effect, PolicyDecisionEffect::ApprovalRequired)
        && !state.premium_governance_enabled()
    {
        approval_error = Some("premium governance approval requests are unavailable".to_string());
    }

    if let Err(error) = link_action_gate_policy_decision(
        state,
        policy_decision_id.as_deref(),
        ctx,
        &links,
        risk_tier,
        action_binding.as_deref(),
        approval_id.as_deref(),
    )
    .await
    {
        approval_error = Some(error.to_string());
    }

    if approval_error.is_none()
        && approval_state
            == crate::app::state::governance_action_gate::ActionGateApprovalState::ApprovedAndConsumed
    {
        return Some(ToolPolicyDecision {
            allowed: true,
            reason: None,
            policy_decision_id,
            dispatch_decision: None,
        });
    }

    let reason = if approval_state
        == crate::app::state::governance_action_gate::ActionGateApprovalState::Denied
        && approval_id.is_some()
    {
        format!("tool `{tool}` denied by runtime approval gate")
    } else {
        format!(
            "tool `{tool}` paused by runtime approval gate ({}): {}{}",
            outcome.reviewer_eligibility.as_str(),
            outcome.reason,
            approval_id
                .as_deref()
                .map(|id| format!(" (approval `{id}`)"))
                .unwrap_or_default()
        )
    };
    if state.is_ready() {
        state.event_bus.publish(EngineEvent::new(
            "approval.gate.tool.gated",
            json!({
                "sessionID": ctx.session_id,
                "messageID": ctx.message_id,
                "tool": tool,
                "riskTier": risk_tier.as_str(),
                "effect": outcome.effect,
                "reviewerEligibility": outcome.reviewer_eligibility.as_str(),
                "reasonCode": outcome.reason_code,
                "timestampMs": crate::now_ms(),
                "tenantContext": tenant_context,
            }),
        ));
    }
    let dispatch_decision = if approval_error.is_none()
        && approval_state
            == crate::app::state::governance_action_gate::ActionGateApprovalState::Pending
    {
        policy_decision_id
            .as_ref()
            .zip(approval_id.as_ref())
            .zip(action_binding.as_ref())
            .map(|((decision_id, approval_id), action_binding)| {
                tandem_tools::ToolDispatchDecision::approval_required(
                    decision_id.clone(),
                    reason.clone(),
                    tandem_tools::ToolDispatchApprovalRequirement {
                        approval_request_id: Some(approval_id.clone()),
                        policy_id: "approval_gate_matrix".to_string(),
                        policy_version_id: "approval_gate_matrix/v1".to_string(),
                        rule_id: outcome.reason_code.clone(),
                        rule_version: 1,
                        approval_class: outcome.reviewer_eligibility.as_str().to_string(),
                        action_binding: action_binding.clone(),
                    },
                )
            })
    } else {
        None
    };
    Some(ToolPolicyDecision {
        allowed: false,
        reason: Some(reason),
        policy_decision_id,
        dispatch_decision,
    })
}