use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OsStatus {
pub runtime: RuntimeStatus,
pub postgres: PostgresStatus,
pub forge: ForgeStatus,
}
impl OsStatus {
pub fn offline() -> Self {
Self {
runtime: RuntimeStatus { version: String::new(), state: ServiceState::Offline },
postgres: PostgresStatus { state: ServiceState::Offline, port: None, data_dir: None },
forge: ForgeStatus { state: ServiceState::Offline, port: None },
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForgeStatus {
pub state: ServiceState,
pub port: Option<u16>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuntimeStatus {
pub version: String,
pub state: ServiceState,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostgresStatus {
pub state: ServiceState,
pub port: Option<u16>,
pub data_dir: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ServiceState {
Online,
Offline,
Starting,
Stopping,
Error,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AppType {
#[default]
App,
Integration,
Agent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ActionDefinition {
pub id: String,
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub input_schema: Option<JsonValue>,
#[serde(default)]
pub output_schema: Option<JsonValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppManifest {
pub app_id: String,
pub name: String,
#[serde(default = "default_version")]
pub version: String,
#[serde(default)]
pub description: String,
#[serde(default, rename = "type")]
pub app_type: AppType,
#[serde(default)]
pub permissions: Option<PermissionsContract>,
#[serde(default)]
pub data_contract: Vec<EntityContract>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub actions: Vec<ActionDefinition>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config_schema: Option<JsonValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user_auth: Option<JsonValue>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub webhooks: Vec<WebhookDefinition>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trigger: Option<TriggerConfig>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub crons: Vec<CronDefinition>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub public: Option<PublicSurface>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicSurface {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub rpcs: Vec<PublicRpc>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub collections: Vec<PublicCollection>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicRpc {
pub name: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub scope: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicCollection {
pub entity: String,
pub actions: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CronDefinition {
pub name: String,
pub schedule: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timezone: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub method: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub payload: Option<JsonValue>,
#[serde(default = "default_overlap_policy")]
pub overlap_policy: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum WebhookDefinition {
Simple(String),
#[serde(rename_all = "camelCase")]
Full { name: String, #[serde(default = "default_post")] method: String },
}
fn default_post() -> String { "POST".into() }
impl WebhookDefinition {
pub fn name(&self) -> &str {
match self {
Self::Simple(s) => s.as_str(),
Self::Full { name, .. } => name.as_str(),
}
}
pub fn method(&self) -> &str {
match self {
Self::Simple(_) => "POST",
Self::Full { method, .. } => method.as_str(),
}
}
}
fn default_overlap_policy() -> String { "skip".into() }
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TriggerConfig {
pub app_id: String,
pub entity: String,
pub on: Vec<String>,
}
fn default_version() -> String {
"0.0.1".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EntityContract {
pub entity_name: String,
pub fields: Vec<FieldContract>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub identity_kind: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub identity_key: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub indexes: Vec<IndexContract>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub checks: Vec<CheckContract>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IndexContract {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub columns: Vec<IndexColumn>,
#[serde(default)]
pub unique: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub using: Option<String>,
#[serde(rename = "where", default, skip_serializing_if = "Option::is_none")]
pub where_clause: Option<String>,
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
pub with: std::collections::BTreeMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum IndexColumn {
Name(String),
Spec(IndexColumnSpec),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckContract {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub expr: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IndexColumnSpec {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub column: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expr: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sort: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nulls: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ops: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldContract {
pub name: String,
#[serde(rename = "type")]
pub field_type: String,
#[serde(default)]
pub required: bool,
#[serde(default)]
pub default_value: Option<JsonValue>,
#[serde(default)]
pub enum_values: Option<Vec<String>>,
#[serde(default)]
pub references: Option<FieldReference>,
#[serde(default)]
pub is_primary_key: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub on_delete: Option<OnDeletePolicy>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OnDeletePolicy {
Cascade,
Restrict,
SetNull,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldReference {
pub entity: String,
pub field: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InstalledApp {
pub id: String,
pub name: String,
pub version: String,
pub status: String,
#[serde(rename = "type", default)]
pub app_type: AppType,
pub entities: Vec<String>,
#[serde(default)]
pub has_frontend: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PermissionsContract {
#[serde(default)]
pub permissions: Vec<PermissionDeclaration>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionDeclaration {
pub key: String,
#[serde(default)]
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaChange {
pub entity: String,
pub change_type: String,
pub column: String,
pub detail: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaVerification {
pub compliant: bool,
pub changes: Vec<SchemaChange>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ProviderType {
Anthropic,
OpenAI,
Bedrock,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentDefinition {
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub system_prompt: Option<String>,
#[serde(default)]
pub memory: Option<AgentMemory>,
#[serde(default)]
pub limits: Option<AgentLimits>,
#[serde(default)]
pub supervision: Option<SupervisionConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentMemory {
pub enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentLimits {
#[serde(default)]
pub max_turns: Option<u32>,
#[serde(default)]
pub max_context_tokens: Option<u64>,
#[serde(default)]
pub keep_recent_messages: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SupervisionConfig {
pub mode: SupervisionMode,
#[serde(default)]
pub policies: Vec<SupervisionPolicy>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SupervisionMode {
Autonomous,
Supervised,
Strict,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SupervisionPolicy {
pub action: String,
#[serde(default)]
pub entity: Option<String>,
#[serde(default)]
pub requires: Option<String>,
#[serde(default)]
pub rate_limit: Option<RateLimit>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateLimit {
pub max: u32,
pub window: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpServerConfig {
pub name: String,
pub transport: McpTransport,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum McpTransport {
Stdio { command: String, #[serde(default)] args: Vec<String> },
Http { url: String, #[serde(default)] headers: std::collections::HashMap<String, String> },
#[deprecated = "use Http"]
Sse { url: String, #[serde(default)] headers: std::collections::HashMap<String, String> },
Cli { install: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolDescriptor {
pub name: String,
pub description: String,
pub input_schema: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowGraph {
pub nodes: Vec<WorkflowNode>,
pub edges: Vec<WorkflowEdge>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowNode {
pub id: String,
pub kind: WorkflowNodeKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default)]
pub params: JsonValue,
#[serde(default)]
pub position: [f32; 2],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase", rename_all_fields = "camelCase")]
pub enum WorkflowNodeKind {
Trigger { trigger: TriggerKind },
Tool { tool_name: String },
Control { control: ControlKind },
Code,
SubWorkflow { workflow_id: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum TriggerKind {
Manual,
Schedule,
Webhook,
RecordChange,
Channel,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ControlKind {
If,
Switch,
Merge,
Set,
Loop,
Wait,
Stop,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowEdge {
pub from: String,
pub to: String,
#[serde(default)]
pub from_output: u8,
#[serde(default)]
pub to_input: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Item {
pub json: JsonValue,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum WorkflowExecutionStatus {
Queued,
Running,
Succeeded,
Failed,
Canceled,
}
impl WorkflowExecutionStatus {
pub fn as_str(self) -> &'static str {
match self {
Self::Queued => "queued",
Self::Running => "running",
Self::Succeeded => "succeeded",
Self::Failed => "failed",
Self::Canceled => "canceled",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum WorkflowNodeRunStatus {
Pending,
Running,
Succeeded,
Failed,
Skipped,
}
impl WorkflowNodeRunStatus {
pub fn as_str(self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Running => "running",
Self::Succeeded => "succeeded",
Self::Failed => "failed",
Self::Skipped => "skipped",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
User,
Assistant,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentBlock {
Text { text: String },
ToolUse { id: String, name: String, input: serde_json::Value },
ToolResult { tool_use_id: String, content: String, #[serde(default)] is_error: bool },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: Role,
pub content: Vec<ContentBlock>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDef {
pub name: String,
pub description: String,
pub input_schema: serde_json::Value,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn index_column_deserializes_bare_string_and_spec() {
let json = r#"{"entityName":"program","fields":[],"indexes":[{"name":"test","columns":["slug"],"unique":true}]}"#;
let e: EntityContract = serde_json::from_str(json).expect("deser failed");
assert_eq!(e.indexes.len(), 1, "indexes should parse");
assert_eq!(e.indexes[0].columns.len(), 1, "columns should parse");
match &e.indexes[0].columns[0] {
IndexColumn::Name(n) => assert_eq!(n, "slug"),
other => panic!("expected Name('slug'), got {:?}", other),
}
}
#[test]
fn field_contract_preserves_snake_case_keys() {
let json = r#"{"name":"status","type":"text","enum_values":["draft","active"],"on_delete":"set_null"}"#;
let f: FieldContract = serde_json::from_str(json).expect("deser failed");
assert_eq!(f.enum_values.as_deref(), Some(&["draft".to_string(), "active".to_string()][..]));
assert_eq!(f.on_delete, Some(OnDeletePolicy::SetNull));
let round: FieldContract =
serde_json::from_value(serde_json::to_value(&f).unwrap()).expect("round-trip failed");
assert_eq!(round.enum_values, f.enum_values, "enum_values lost on round-trip");
assert_eq!(round.on_delete, f.on_delete, "on_delete lost on round-trip");
}
#[test]
fn node_kind_round_trips_camelcase_wire() {
let cases = [
("tool", serde_json::json!({"type": "tool", "toolName": "query_data"})),
("trigger", serde_json::json!({"type": "trigger", "trigger": "recordChange"})),
("control", serde_json::json!({"type": "control", "control": "if"})),
("subWorkflow", serde_json::json!({"type": "subWorkflow", "workflowId": "wf-1"})),
];
for (label, wire) in &cases {
let kind: WorkflowNodeKind = serde_json::from_value(wire.clone())
.unwrap_or_else(|e| panic!("[{label}] wire shape rejected: {e}"));
assert_eq!(&serde_json::to_value(&kind).unwrap(), wire,
"[{label}] re-serialized shape drifted from the editor's wire");
}
}
#[test]
fn entity_parses_declarative_checks() {
let json = r#"{"entityName":"enrollment","fields":[],
"checks":[{"name":"enrollment_dates_chk","expr":"exit_date IS NULL OR exit_date >= intake_date"},
{"expr":"x <> y"}]}"#;
let e: EntityContract = serde_json::from_str(json).expect("deser failed");
assert_eq!(e.checks.len(), 2);
assert_eq!(e.checks[0].name.as_deref(), Some("enrollment_dates_chk"));
assert_eq!(e.checks[1].name, None, "unnamed check keeps name None (auto-derived later)");
assert_eq!(e.checks[1].expr, "x <> y");
}
}