prospero_types/automation.rs
1//! Automations (#220): a stored spawn template plus the trigger that fires it.
2//!
3//! **No secret appears in this module.** A webhook automation authenticates
4//! callers with an HMAC key, and that key lives on `prospero-core`'s
5//! `StoredAutomation` instead — so no API response, dashboard view or CLI
6//! listing built from these types can leak it, by construction rather than by
7//! everyone remembering to redact.
8
9use serde::{Deserialize, Serialize};
10
11/// What starts an automation.
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(tag = "kind", rename_all = "snake_case")]
14#[cfg_attr(feature = "openapi", derive(schemars::JsonSchema))]
15pub enum Trigger {
16 /// A cron schedule, evaluated in UTC. Standard 5-field expressions work;
17 /// the 6-field (seconds-first) form is accepted too.
18 Cron {
19 /// The cron expression.
20 schedule: String,
21 },
22 /// An inbound signed request to `POST /api/automations/{id}/trigger`.
23 Webhook,
24}
25
26/// The spawn an automation performs each time it fires.
27///
28/// Mirrors the fields of a manual spawn, so an automation can do nothing a
29/// person with `operate` could not do by hand.
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31#[cfg_attr(feature = "openapi", derive(schemars::JsonSchema))]
32pub struct SpawnTemplate {
33 /// The agent's task. For webhook triggers this may contain
34 /// `{{ dotted.path }}` placeholders filled from the request payload.
35 pub task: String,
36 /// Optional human-readable label for spawned agents.
37 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub label: Option<String>,
39 /// Optional model override.
40 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub model: Option<String>,
42 /// Optional tool allowlist.
43 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub tool_allowlist: Option<Vec<String>>,
45 /// Optional agent-template / frontmatter markdown path.
46 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub frontmatter_path: Option<String>,
48 /// Which of the workspace's named providers to bind (k8s config plane).
49 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub provider_ref: Option<String>,
51 /// Kill the spawned agent after this many seconds (#221).
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub timeout_secs: Option<u64>,
54 /// Run in an isolated git worktree. Defaults to `true`.
55 #[serde(default = "yes")]
56 pub isolation_worktree: bool,
57 /// How the spawned session handles tool calls needing permission (#238).
58 ///
59 /// Defaults to `Supervised`, the same fail-closed default a manual spawn
60 /// gets. An automation runs with nobody watching, which is an argument for
61 /// `Unattended` — but it is the operator's argument to make explicitly,
62 /// not something creating an automation should confer by itself.
63 #[serde(default)]
64 pub permission_posture: crate::model::PermissionPosture,
65}
66
67fn yes() -> bool {
68 true
69}
70
71impl Default for SpawnTemplate {
72 fn default() -> Self {
73 SpawnTemplate {
74 task: String::new(),
75 label: None,
76 model: None,
77 tool_allowlist: None,
78 frontmatter_path: None,
79 provider_ref: None,
80 timeout_secs: None,
81 isolation_worktree: true,
82 permission_posture: crate::model::PermissionPosture::default(),
83 }
84 }
85}
86
87/// A configured automation.
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89#[cfg_attr(feature = "openapi", derive(schemars::JsonSchema))]
90pub struct Automation {
91 /// Operator-chosen id (the registry key, unique across the fleet).
92 pub id: String,
93 /// Target workspace name.
94 pub workspace: String,
95 /// What fires it.
96 pub trigger: Trigger,
97 /// What it spawns.
98 pub template: SpawnTemplate,
99 /// Disabled automations are kept but never fire.
100 pub enabled: bool,
101 /// RFC 3339 creation time. Doubles as the scheduling anchor before the
102 /// first fire, so a new cron automation waits for its next tick rather
103 /// than firing the moment it is saved.
104 pub created_at: String,
105 /// RFC 3339 time of the last fire, if any.
106 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub last_fired_at: Option<String>,
108}
109
110/// What caused a run.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case")]
113#[cfg_attr(feature = "openapi", derive(schemars::JsonSchema))]
114pub enum RunSource {
115 /// A cron tick came due.
116 Schedule,
117 /// A signed webhook arrived.
118 Webhook,
119 /// Someone asked for it explicitly ("run now").
120 Manual,
121}
122
123/// One recorded firing of an automation, successful or not.
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125#[cfg_attr(feature = "openapi", derive(schemars::JsonSchema))]
126pub struct AutomationRun {
127 /// The automation that fired.
128 pub automation_id: String,
129 /// RFC 3339 time of the firing.
130 pub fired_at: String,
131 /// What caused it.
132 pub source: RunSource,
133 /// The agent it spawned, absent if the spawn failed.
134 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub agent_id: Option<String>,
136 /// Why it failed, absent on success. A run is recorded either way — an
137 /// automation that silently stops working is the failure mode run history
138 /// exists to prevent.
139 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub error: Option<String>,
141}
142
143/// Body for `POST /api/automations`.
144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
145#[cfg_attr(feature = "openapi", derive(schemars::JsonSchema))]
146pub struct CreateAutomationBody {
147 /// Operator-chosen id.
148 pub id: String,
149 /// Target workspace name.
150 pub workspace: String,
151 /// What fires it.
152 pub trigger: Trigger,
153 /// What it spawns.
154 pub template: SpawnTemplate,
155 /// Create it disabled. Defaults to enabled.
156 #[serde(default = "yes")]
157 pub enabled: bool,
158}
159
160/// Response to `POST /api/automations`.
161#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
162#[cfg_attr(feature = "openapi", derive(schemars::JsonSchema))]
163pub struct CreatedAutomationResponse {
164 /// The stored automation.
165 pub automation: Automation,
166 /// For a webhook trigger, the generated signing key — **returned here and
167 /// never again**, like an API token. Senders sign their request body with
168 /// it and send `X-Prospero-Signature: sha256=<hex>`.
169 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub webhook_secret: Option<String>,
171}
172
173/// Body for `PUT /api/automations/{id}/enabled`.
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
175#[cfg_attr(feature = "openapi", derive(schemars::JsonSchema))]
176pub struct SetEnabledBody {
177 /// Whether the automation should fire.
178 pub enabled: bool,
179}
180
181/// Response to a fire request (`run now`, or an accepted webhook).
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
183#[cfg_attr(feature = "openapi", derive(schemars::JsonSchema))]
184pub struct FiredResponse {
185 /// The recorded run.
186 pub run: AutomationRun,
187}