1use serde::{Deserialize, Serialize};
2use time::OffsetDateTime;
3
4pub type WorkflowImportId = String;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
7#[serde(rename_all = "camelCase")]
8pub enum WorkflowSourceType {
9 Guidance,
10 Skill,
11 McpServer,
12 SlashCommand,
13 Hook,
14 Plugin,
15 Unknown,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
19#[serde(rename_all = "camelCase")]
20pub enum WorkflowImportState {
21 Detected,
22 Previewed,
23 Enabled,
24 Ignored,
25 Disabled,
26 Removed,
27 Stale,
28 Failed,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
32#[serde(rename_all = "camelCase")]
33pub enum WorkflowImportRisk {
34 Passive,
35 ReadsWorkspace,
36 StartsProcess,
37 RunsHook,
38 Unknown,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
42#[serde(rename_all = "camelCase")]
43pub struct WorkflowSource {
44 pub source_type: WorkflowSourceType,
45 pub path: String,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub name: Option<String>,
48 pub hash: String,
49 #[serde(with = "time::serde::rfc3339")]
50 pub detected_at: OffsetDateTime,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
54#[serde(rename_all = "camelCase")]
55pub struct WorkflowConflict {
56 pub field: String,
57 pub existing: String,
58 pub incoming: String,
59 pub detail: String,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
63#[serde(rename_all = "camelCase")]
64pub struct WorkflowImportItem {
65 pub id: WorkflowImportId,
66 pub title: String,
67 pub summary: String,
68 pub source: WorkflowSource,
69 pub state: WorkflowImportState,
70 pub risk: WorkflowImportRisk,
71 #[serde(default)]
72 pub command_capable: bool,
73 #[serde(default)]
74 pub approval_required: bool,
75 #[serde(default)]
76 pub preview: serde_json::Value,
77 #[serde(default, skip_serializing_if = "Vec::is_empty")]
78 pub conflicts: Vec<WorkflowConflict>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 #[serde(with = "time::serde::rfc3339::option")]
81 pub enabled_at: Option<OffsetDateTime>,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
85#[serde(rename_all = "camelCase")]
86pub enum WorkflowImportDecisionKind {
87 Preview,
88 Enable,
89 Ignore,
90 Disable,
91 Refresh,
92 Remove,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
96#[serde(rename_all = "camelCase")]
97pub struct WorkflowImportDecision {
98 pub item_id: WorkflowImportId,
99 pub decision: WorkflowImportDecisionKind,
100 pub source_hash: String,
101 #[serde(default)]
102 pub approved_side_effects: bool,
103 #[serde(with = "time::serde::rfc3339")]
104 pub decided_at: OffsetDateTime,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
108#[serde(rename_all = "camelCase")]
109pub struct WorkflowImportScan {
110 pub workspace: String,
111 pub items: Vec<WorkflowImportItem>,
112 #[serde(default, skip_serializing_if = "Vec::is_empty")]
113 pub errors: Vec<WorkflowImportError>,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
117#[serde(rename_all = "camelCase")]
118pub struct WorkflowImportError {
119 pub path: String,
120 pub message: String,
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn workflow_import_records_are_camel_case_and_source_attributed() {
129 let item = WorkflowImportItem {
130 id: "skill-demo".to_string(),
131 title: "Demo Skill".to_string(),
132 summary: "Imported skill".to_string(),
133 source: WorkflowSource {
134 source_type: WorkflowSourceType::Skill,
135 path: ".agents/skills/demo/SKILL.md".to_string(),
136 name: Some("demo".to_string()),
137 hash: "abc123".to_string(),
138 detected_at: OffsetDateTime::UNIX_EPOCH,
139 },
140 state: WorkflowImportState::Detected,
141 risk: WorkflowImportRisk::Passive,
142 command_capable: false,
143 approval_required: false,
144 preview: serde_json::json!({ "description": "safe" }),
145 conflicts: Vec::new(),
146 enabled_at: None,
147 };
148
149 let value = serde_json::to_value(&item).unwrap();
150
151 assert_eq!(value["source"]["sourceType"], "skill");
152 assert_eq!(value["source"]["detectedAt"], "1970-01-01T00:00:00Z");
153 assert_eq!(value["commandCapable"], false);
154 assert_eq!(value["source"]["path"], ".agents/skills/demo/SKILL.md");
155 }
156
157 #[test]
158 fn command_capable_imports_can_require_approval() {
159 let decision = WorkflowImportDecision {
160 item_id: "mcp-server-local".to_string(),
161 decision: WorkflowImportDecisionKind::Enable,
162 source_hash: "hash".to_string(),
163 approved_side_effects: true,
164 decided_at: OffsetDateTime::UNIX_EPOCH,
165 };
166
167 let value = serde_json::to_value(decision).unwrap();
168
169 assert_eq!(value["approvedSideEffects"], true);
170 assert_eq!(value["decision"], "enable");
171 }
172}