Skip to main content

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}