tandem-server 0.6.5

HTTP server for Tandem engine APIs
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tandem_types::{DataClass, PrincipalRef, ResourceScope, TenantContext, ToolRiskTier};

use super::phases::{StatefulWorkflowPhase, StatefulWorkflowPhaseTransitionRecord};

pub const STATEFUL_RUNTIME_SCHEMA_VERSION: u32 = 1;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StatefulRuntimeScope {
    #[serde(default = "default_schema_version")]
    pub schema_version: u32,
    #[serde(default)]
    pub tenant_context: TenantContext,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub owning_org_unit_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub owner_principal: Option<PrincipalRef>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub resource_scope: Option<ResourceScope>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub data_classes: Vec<DataClass>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub risk_tier: Option<ToolRiskTier>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub policy_version_id: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub delegation_grant_ids: Vec<String>,
}

impl Default for StatefulRuntimeScope {
    fn default() -> Self {
        Self::local_implicit()
    }
}

impl StatefulRuntimeScope {
    pub fn from_tenant_context(tenant_context: TenantContext) -> Self {
        Self {
            schema_version: STATEFUL_RUNTIME_SCHEMA_VERSION,
            tenant_context,
            owning_org_unit_id: None,
            owner_principal: None,
            resource_scope: None,
            data_classes: Vec::new(),
            risk_tier: None,
            policy_version_id: None,
            delegation_grant_ids: Vec::new(),
        }
    }

    pub fn local_implicit() -> Self {
        Self::from_tenant_context(TenantContext::local_implicit())
    }

    pub fn organization_id(&self) -> &str {
        &self.tenant_context.org_id
    }

    pub fn workspace_id(&self) -> &str {
        &self.tenant_context.workspace_id
    }

    pub fn deployment_id(&self) -> Option<&str> {
        self.tenant_context.deployment_id.as_deref()
    }

    pub fn visible_to_tenant(&self, tenant: &TenantContext) -> bool {
        if tenant.is_local_implicit() {
            return true;
        }
        self.tenant_context.org_id == tenant.org_id
            && self.tenant_context.workspace_id == tenant.workspace_id
            && self.tenant_context.deployment_id == tenant.deployment_id
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum StatefulWorkflowRunKind {
    AutomationV2,
    Workflow,
    ContextRun,
    Unknown,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum StatefulWorkflowRunStatus {
    Queued,
    Running,
    Sleeping,
    AwaitingWebhook,
    AwaitingApproval,
    Pausing,
    Paused,
    Retrying,
    Blocked,
    Completed,
    Failed,
    Cancelled,
    DeadLettered,
    DryRun,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum StatefulWaitKind {
    Timer,
    Webhook,
    Approval,
    ExternalCondition,
    RetryBackoff,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum StatefulWaitStatus {
    Waiting,
    Claimed,
    Woken,
    TimedOut,
    Escalated,
    Cancelled,
}

impl StatefulWaitStatus {
    pub fn is_terminal(&self) -> bool {
        matches!(
            self,
            Self::Woken | Self::TimedOut | Self::Escalated | Self::Cancelled
        )
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum StatefulWaitTimeoutAction {
    Cancel,
    Escalate,
    Remind,
    Resume,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StatefulWaitTimeoutPolicy {
    pub timeout_at_ms: u64,
    pub on_timeout: StatefulWaitTimeoutAction,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub escalate_to: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub remind_every_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub metadata: Option<Value>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct StatefulWebhookWaitMatch {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub trigger_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub provider: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub provider_event_kind: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub provider_event_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub body_digest: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub idempotency_key: Option<String>,
}

impl StatefulWebhookWaitMatch {
    pub fn has_constraint(&self) -> bool {
        self.trigger_id.is_some()
            || self.provider.is_some()
            || self.provider_event_kind.is_some()
            || self.provider_event_id.is_some()
            || self.body_digest.is_some()
            || self.idempotency_key.is_some()
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct StatefulWebhookWaitEvent {
    pub trigger_id: String,
    pub provider: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub provider_event_kind: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub provider_event_id: Option<String>,
    pub body_digest: String,
    pub idempotency_key: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StatefulWaitRecord {
    #[serde(default = "default_schema_version")]
    pub schema_version: u32,
    pub wait_id: String,
    pub run_id: String,
    pub wait_kind: StatefulWaitKind,
    pub status: StatefulWaitStatus,
    pub scope: StatefulRuntimeScope,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub phase_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
    pub created_at_ms: u64,
    pub updated_at_ms: u64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub wake_at_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub timeout_policy: Option<StatefulWaitTimeoutPolicy>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub event_seq: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub wake_idempotency_key: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub claimed_by: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub claimed_at_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub claim_expires_at_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub completed_at_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub metadata: Option<Value>,
}

impl StatefulWaitRecord {
    pub fn visible_to_tenant(&self, tenant: &TenantContext) -> bool {
        self.scope.visible_to_tenant(tenant)
    }

    pub fn is_due_at(&self, now_ms: u64) -> bool {
        self.status == StatefulWaitStatus::Waiting
            && self
                .wake_at_ms
                .map(|wake_at_ms| wake_at_ms <= now_ms)
                .unwrap_or(false)
    }

    pub fn claim_is_active_at(&self, now_ms: u64) -> bool {
        self.status == StatefulWaitStatus::Claimed
            && self
                .claim_expires_at_ms
                .map(|expires_at_ms| expires_at_ms > now_ms)
                .unwrap_or(false)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StatefulWorkflowRunRecord {
    #[serde(default = "default_schema_version")]
    pub schema_version: u32,
    pub run_id: String,
    pub kind: StatefulWorkflowRunKind,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workflow_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub automation_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub automation_run_id: Option<String>,
    pub scope: StatefulRuntimeScope,
    pub status: StatefulWorkflowRunStatus,
    #[serde(default)]
    pub phase: StatefulWorkflowPhase,
    #[serde(default)]
    pub phase_history: Vec<StatefulWorkflowPhaseTransitionRecord>,
    #[serde(default)]
    pub allowed_next_phases: Vec<StatefulWorkflowPhase>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub trigger_type: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub trigger_event: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub source_event_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub task_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub current_phase_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub active_wait_kind: Option<StatefulWaitKind>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub active_wait_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workflow_definition_version: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workflow_definition_snapshot_hash: Option<String>,
    pub created_at_ms: u64,
    pub updated_at_ms: u64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub started_at_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub finished_at_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub latest_snapshot_id: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub related_context_run_ids: Vec<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub metadata: Option<Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StatefulRunEventRecord {
    #[serde(default = "default_schema_version")]
    pub schema_version: u32,
    pub event_id: String,
    pub run_id: String,
    pub seq: u64,
    pub event_type: String,
    pub occurred_at_ms: u64,
    pub scope: StatefulRuntimeScope,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub actor: Option<PrincipalRef>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub phase_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub phase_transition: Option<StatefulWorkflowPhaseTransitionRecord>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub wait_kind: Option<StatefulWaitKind>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub causation_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub correlation_id: Option<String>,
    #[serde(default)]
    pub payload: Value,
}

impl StatefulRunEventRecord {
    pub fn visible_to_tenant(&self, tenant: &TenantContext) -> bool {
        self.scope.visible_to_tenant(tenant)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StatefulRunSnapshotRecord {
    #[serde(default = "default_schema_version")]
    pub schema_version: u32,
    pub snapshot_id: String,
    pub run_id: String,
    pub seq: u64,
    pub created_at_ms: u64,
    pub scope: StatefulRuntimeScope,
    pub status: StatefulWorkflowRunStatus,
    #[serde(default)]
    pub phase: StatefulWorkflowPhase,
    #[serde(default)]
    pub phase_history: Vec<StatefulWorkflowPhaseTransitionRecord>,
    #[serde(default)]
    pub allowed_next_phases: Vec<StatefulWorkflowPhase>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub phase_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub source_record_kind: Option<StatefulWorkflowRunKind>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub checkpoint: Option<Value>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub payload_digest: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workflow_definition_version: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workflow_definition_snapshot_hash: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub metadata: Option<Value>,
}

impl StatefulRunSnapshotRecord {
    pub fn visible_to_tenant(&self, tenant: &TenantContext) -> bool {
        self.scope.visible_to_tenant(tenant)
    }
}

pub fn default_schema_version() -> u32 {
    STATEFUL_RUNTIME_SCHEMA_VERSION
}