1use serde::{Deserialize, Serialize};
2use serde_json::Value as JsonValue;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct OsStatus {
6 pub runtime: RuntimeStatus,
7 pub postgres: PostgresStatus,
8 pub forge: ForgeStatus,
9}
10
11impl OsStatus {
12 pub fn offline() -> Self {
13 Self {
14 runtime: RuntimeStatus { version: String::new(), state: ServiceState::Offline },
15 postgres: PostgresStatus { state: ServiceState::Offline, port: None, data_dir: None },
16 forge: ForgeStatus { state: ServiceState::Offline, port: None },
17 }
18 }
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ForgeStatus {
23 pub state: ServiceState,
24 pub port: Option<u16>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct RuntimeStatus {
29 pub version: String,
30 pub state: ServiceState,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct PostgresStatus {
35 pub state: ServiceState,
36 pub port: Option<u16>,
37 pub data_dir: Option<String>,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(rename_all = "lowercase")]
42pub enum ServiceState {
43 Online,
44 Offline,
45 Starting,
46 Stopping,
47 Error,
48}
49
50#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "lowercase")]
52pub enum AppType {
53 #[default]
54 App,
55 Integration,
56 Agent,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase")]
61pub struct ActionDefinition {
62 pub id: String,
63 pub name: String,
64 #[serde(default)]
65 pub description: String,
66 #[serde(default)]
67 pub input_schema: Option<JsonValue>,
68 #[serde(default)]
69 pub output_schema: Option<JsonValue>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73#[serde(rename_all = "camelCase")]
74pub struct AppManifest {
75 pub app_id: String,
76 pub name: String,
77 #[serde(default = "default_version")]
78 pub version: String,
79 #[serde(default)]
80 pub description: String,
81 #[serde(default, rename = "type")]
82 pub app_type: AppType,
83 #[serde(default)]
84 pub permissions: Option<PermissionsContract>,
85 #[serde(default)]
86 pub data_contract: Vec<EntityContract>,
87 #[serde(default, skip_serializing_if = "Vec::is_empty")]
88 pub actions: Vec<ActionDefinition>,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
90 pub config_schema: Option<JsonValue>,
91 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub user_auth: Option<JsonValue>,
93 #[serde(default, skip_serializing_if = "Vec::is_empty")]
94 pub webhooks: Vec<WebhookDefinition>,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub instructions: Option<String>,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub trigger: Option<TriggerConfig>,
101 #[serde(default, skip_serializing_if = "Vec::is_empty")]
103 pub crons: Vec<CronDefinition>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub icon: Option<String>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub public: Option<PublicSurface>,
111}
112
113#[derive(Debug, Clone, Default, Serialize, Deserialize)]
120#[serde(rename_all = "camelCase")]
121pub struct PublicSurface {
122 #[serde(default, skip_serializing_if = "Vec::is_empty")]
126 pub rpcs: Vec<PublicRpc>,
127 #[serde(default, skip_serializing_if = "Vec::is_empty")]
130 pub collections: Vec<PublicCollection>,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct PublicRpc {
136 pub name: String,
137 #[serde(default, skip_serializing_if = "Vec::is_empty")]
142 pub scope: Vec<String>,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct PublicCollection {
148 pub entity: String,
149 pub actions: Vec<String>,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
154#[serde(rename_all = "camelCase")]
155pub struct CronDefinition {
156 pub name: String,
157 pub schedule: String,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub timezone: Option<String>,
160 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub method: Option<String>,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub payload: Option<JsonValue>,
164 #[serde(default = "default_overlap_policy")]
165 pub overlap_policy: String,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
169#[serde(untagged)]
170pub enum WebhookDefinition {
171 Simple(String),
172 #[serde(rename_all = "camelCase")]
173 Full { name: String, #[serde(default = "default_post")] method: String },
174}
175
176fn default_post() -> String { "POST".into() }
177
178impl WebhookDefinition {
179 pub fn name(&self) -> &str {
180 match self {
181 Self::Simple(s) => s.as_str(),
182 Self::Full { name, .. } => name.as_str(),
183 }
184 }
185 pub fn method(&self) -> &str {
186 match self {
187 Self::Simple(_) => "POST",
188 Self::Full { method, .. } => method.as_str(),
189 }
190 }
191}
192
193fn default_overlap_policy() -> String { "skip".into() }
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct TriggerConfig {
198 pub app_id: String,
199 pub entity: String,
200 pub on: Vec<String>,
201}
202
203fn default_version() -> String {
204 "0.0.1".to_string()
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
208#[serde(rename_all = "camelCase")]
209pub struct EntityContract {
210 pub entity_name: String,
211 pub fields: Vec<FieldContract>,
212 #[serde(default, skip_serializing_if = "Option::is_none")]
213 pub identity_kind: Option<String>,
214 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub identity_key: Option<String>,
216 #[serde(default, skip_serializing_if = "Vec::is_empty")]
220 pub indexes: Vec<IndexContract>,
221 #[serde(default, skip_serializing_if = "Vec::is_empty")]
226 pub checks: Vec<CheckContract>,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
233#[serde(rename_all = "camelCase")]
234pub struct IndexContract {
235 #[serde(default, skip_serializing_if = "Option::is_none")]
236 pub name: Option<String>,
237 pub columns: Vec<IndexColumn>,
238 #[serde(default)]
239 pub unique: bool,
240 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub using: Option<String>,
243 #[serde(rename = "where", default, skip_serializing_if = "Option::is_none")]
245 pub where_clause: Option<String>,
246 #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
248 pub with: std::collections::BTreeMap<String, String>,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
254#[serde(untagged)]
255pub enum IndexColumn {
256 Name(String),
257 Spec(IndexColumnSpec),
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct CheckContract {
266 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub name: Option<String>,
268 pub expr: String,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
272#[serde(rename_all = "camelCase")]
273pub struct IndexColumnSpec {
274 #[serde(default, skip_serializing_if = "Option::is_none")]
275 pub column: Option<String>,
276 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub expr: Option<String>,
279 #[serde(default, skip_serializing_if = "Option::is_none")]
281 pub sort: Option<String>,
282 #[serde(default, skip_serializing_if = "Option::is_none")]
284 pub nulls: Option<String>,
285 #[serde(default, skip_serializing_if = "Option::is_none")]
287 pub ops: Option<String>,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct FieldContract {
296 pub name: String,
297 #[serde(rename = "type")]
298 pub field_type: String,
299 #[serde(default)]
300 pub required: bool,
301 #[serde(default)]
302 pub default_value: Option<JsonValue>,
303 #[serde(default)]
304 pub enum_values: Option<Vec<String>>,
305 #[serde(default)]
306 pub references: Option<FieldReference>,
307 #[serde(default)]
308 pub is_primary_key: Option<bool>,
309 #[serde(default, skip_serializing_if = "Option::is_none")]
310 pub on_delete: Option<OnDeletePolicy>,
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
314#[serde(rename_all = "snake_case")]
315pub enum OnDeletePolicy {
316 Cascade,
317 Restrict,
318 SetNull,
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct FieldReference {
323 pub entity: String,
324 pub field: String,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize)]
328#[serde(rename_all = "camelCase")]
329pub struct InstalledApp {
330 pub id: String,
331 pub name: String,
332 pub version: String,
333 pub status: String,
334 #[serde(rename = "type", default)]
335 pub app_type: AppType,
336 pub entities: Vec<String>,
337 #[serde(default)]
338 pub has_frontend: bool,
339 #[serde(default, skip_serializing_if = "Option::is_none")]
340 pub icon: Option<String>,
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize)]
344#[serde(rename_all = "camelCase")]
345pub struct PermissionsContract {
346 #[serde(default)]
347 pub permissions: Vec<PermissionDeclaration>,
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct PermissionDeclaration {
352 pub key: String,
353 #[serde(default)]
354 pub description: String,
355}
356
357#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct SchemaChange {
359 pub entity: String,
360 pub change_type: String,
361 pub column: String,
362 pub detail: Option<String>,
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize)]
366pub struct SchemaVerification {
367 pub compliant: bool,
368 pub changes: Vec<SchemaChange>,
369}
370
371#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
372#[serde(rename_all = "lowercase")]
373pub enum ProviderType {
374 Anthropic,
375 OpenAI,
376 Bedrock,
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize)]
380#[serde(rename_all = "camelCase")]
381pub struct AgentDefinition {
382 pub name: String,
383 #[serde(default)]
384 pub description: Option<String>,
385 #[serde(default)]
386 pub system_prompt: Option<String>,
387 #[serde(default)]
388 pub memory: Option<AgentMemory>,
389 #[serde(default)]
390 pub limits: Option<AgentLimits>,
391 #[serde(default)]
392 pub supervision: Option<SupervisionConfig>,
393}
394
395#[derive(Debug, Clone, Serialize, Deserialize)]
396pub struct AgentMemory {
397 pub enabled: bool,
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize)]
401#[serde(rename_all = "camelCase")]
402pub struct AgentLimits {
403 #[serde(default)]
404 pub max_turns: Option<u32>,
405 #[serde(default)]
406 pub max_context_tokens: Option<u64>,
407 #[serde(default)]
408 pub keep_recent_messages: Option<u32>,
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize)]
412#[serde(rename_all = "camelCase")]
413pub struct SupervisionConfig {
414 pub mode: SupervisionMode,
415 #[serde(default)]
416 pub policies: Vec<SupervisionPolicy>,
417}
418
419#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
420#[serde(rename_all = "lowercase")]
421pub enum SupervisionMode {
422 Autonomous,
423 Supervised,
424 Strict,
425}
426
427#[derive(Debug, Clone, Serialize, Deserialize)]
428#[serde(rename_all = "camelCase")]
429pub struct SupervisionPolicy {
430 pub action: String,
431 #[serde(default)]
432 pub entity: Option<String>,
433 #[serde(default)]
434 pub requires: Option<String>,
435 #[serde(default)]
436 pub rate_limit: Option<RateLimit>,
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize)]
440pub struct RateLimit {
441 pub max: u32,
442 pub window: String,
443}
444
445#[derive(Debug, Clone, Serialize, Deserialize)]
446#[serde(rename_all = "camelCase")]
447pub struct McpServerConfig {
448 pub name: String,
449 pub transport: McpTransport,
450}
451
452#[derive(Debug, Clone, Serialize, Deserialize)]
453#[serde(tag = "type", rename_all = "lowercase")]
454pub enum McpTransport {
455 Stdio { command: String, #[serde(default)] args: Vec<String> },
456 Http { url: String, #[serde(default)] headers: std::collections::HashMap<String, String> },
457 #[deprecated = "use Http"]
458 Sse { url: String, #[serde(default)] headers: std::collections::HashMap<String, String> },
459 Cli { install: String },
460}
461
462#[derive(Debug, Clone, Serialize, Deserialize)]
463#[serde(rename_all = "camelCase")]
464pub struct ToolDescriptor {
465 pub name: String,
466 pub description: String,
467 pub input_schema: serde_json::Value,
468}
469
470#[derive(Debug, Clone, Serialize, Deserialize)]
471#[serde(rename_all = "lowercase")]
472pub enum Role {
473 User,
474 Assistant,
475}
476
477#[derive(Debug, Clone, Serialize, Deserialize)]
478#[serde(tag = "type", rename_all = "snake_case")]
479pub enum ContentBlock {
480 Text { text: String },
481 ToolUse { id: String, name: String, input: serde_json::Value },
482 ToolResult { tool_use_id: String, content: String, #[serde(default)] is_error: bool },
483}
484
485#[derive(Debug, Clone, Serialize, Deserialize)]
486pub struct ChatMessage {
487 pub role: Role,
488 pub content: Vec<ContentBlock>,
489}
490
491#[derive(Debug, Clone, Serialize, Deserialize)]
492pub struct ToolDef {
493 pub name: String,
494 pub description: String,
495 pub input_schema: serde_json::Value,
496}
497
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502 #[test]
503 fn index_column_deserializes_bare_string_and_spec() {
504 let json = r#"{"entityName":"program","fields":[],"indexes":[{"name":"test","columns":["slug"],"unique":true}]}"#;
505 let e: EntityContract = serde_json::from_str(json).expect("deser failed");
506 assert_eq!(e.indexes.len(), 1, "indexes should parse");
507 assert_eq!(e.indexes[0].columns.len(), 1, "columns should parse");
508 match &e.indexes[0].columns[0] {
509 IndexColumn::Name(n) => assert_eq!(n, "slug"),
510 other => panic!("expected Name('slug'), got {:?}", other),
511 }
512 }
513
514 #[test]
518 fn field_contract_preserves_snake_case_keys() {
519 let json = r#"{"name":"status","type":"text","enum_values":["draft","active"],"on_delete":"set_null"}"#;
520 let f: FieldContract = serde_json::from_str(json).expect("deser failed");
521 assert_eq!(f.enum_values.as_deref(), Some(&["draft".to_string(), "active".to_string()][..]));
522 assert_eq!(f.on_delete, Some(OnDeletePolicy::SetNull));
523
524 let round: FieldContract =
527 serde_json::from_value(serde_json::to_value(&f).unwrap()).expect("round-trip failed");
528 assert_eq!(round.enum_values, f.enum_values, "enum_values lost on round-trip");
529 assert_eq!(round.on_delete, f.on_delete, "on_delete lost on round-trip");
530 }
531
532 #[test]
533 fn entity_parses_declarative_checks() {
534 let json = r#"{"entityName":"enrollment","fields":[],
535 "checks":[{"name":"enrollment_dates_chk","expr":"exit_date IS NULL OR exit_date >= intake_date"},
536 {"expr":"x <> y"}]}"#;
537 let e: EntityContract = serde_json::from_str(json).expect("deser failed");
538 assert_eq!(e.checks.len(), 2);
539 assert_eq!(e.checks[0].name.as_deref(), Some("enrollment_dates_chk"));
540 assert_eq!(e.checks[1].name, None, "unnamed check keeps name None (auto-derived later)");
541 assert_eq!(e.checks[1].expr, "x <> y");
542 }
543}