byteor_actions/
resolver.rs

1//! Stage resolver wiring for ActionGraph side-effecting stages.
2
3use byteor_core::config::Environment;
4use byteor_pipeline_exec::{ResolvedStage, StageResolver};
5
6/// Prefix for the HTTP action-stage family.
7pub const STAGE_KEY_HTTP_POST_PREFIX: &str = "http_post:";
8
9/// Prefix for the exec action-stage family.
10pub const STAGE_KEY_EXEC_PREFIX: &str = "exec:";
11
12/// Static metadata for one parameterized action-stage family.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct ActionStageFamily {
15    /// Stable prefix published to authoring and deploy surfaces.
16    pub key_prefix: &'static str,
17    /// Short operator-facing description.
18    pub description: &'static str,
19    /// Approval capability ids implied by this family.
20    pub approval_capabilities: &'static [&'static str],
21}
22
23const ACTION_STAGE_FAMILIES: &[ActionStageFamily] = &[
24    ActionStageFamily {
25        key_prefix: STAGE_KEY_HTTP_POST_PREFIX,
26        description: "HTTP POST side-effect stage family",
27        approval_capabilities: &["execute_side_effects", "http_post"],
28    },
29    ActionStageFamily {
30        key_prefix: STAGE_KEY_EXEC_PREFIX,
31        description: "Process execution side-effect stage family",
32        approval_capabilities: &["execute_side_effects", "exec"],
33    },
34];
35
36/// Return the published action-stage families.
37pub fn stage_families() -> &'static [ActionStageFamily] {
38    ACTION_STAGE_FAMILIES
39}
40
41/// Resolver for ActionGraph side-effecting stages.
42///
43/// This resolver recognizes stage keys:
44/// - `http_post:<url>`
45/// - `exec:<program>|<arg>|...`
46///
47/// Policy decisions are evaluated per stage key and may disable side effects under dry-run.
48pub struct ActionStages {
49    policy: byteor_policy::Policy,
50    is_replay: bool,
51    dry_run: bool,
52    approval_token_present: bool,
53    replay: byteor_policy::ReplayAccounting,
54}
55
56impl ActionStages {
57    /// Construct a resolver for ActionGraph actions.
58    pub fn new(
59        policy: byteor_policy::Policy,
60        is_replay: bool,
61        dry_run: bool,
62        approval_token_present: bool,
63        replay: byteor_policy::ReplayAccounting,
64    ) -> Self {
65        Self {
66            policy,
67            is_replay,
68            dry_run,
69            approval_token_present,
70            replay,
71        }
72    }
73
74    fn decide(&self, name: &str) -> byteor_policy::Decision {
75        let req = byteor_policy::ActionRequest {
76            env: self.policy.env,
77            is_replay: self.is_replay,
78            dry_run: self.dry_run,
79            action: byteor_policy::ActionDecl {
80                name,
81                capability: byteor_policy::ActionCapability::SideEffecting,
82            },
83            approval_token_present: self.approval_token_present,
84            replay: self.replay,
85        };
86        byteor_policy::decide(&self.policy, &req)
87    }
88
89    /// Convenience: create a policy bundle from safe defaults.
90    pub fn safe_defaults(env: Environment) -> byteor_policy::Policy {
91        byteor_policy::Policy::safe_defaults(env)
92    }
93}
94
95impl StageResolver for ActionStages {
96    fn resolve(&self, stage: &str) -> Option<ResolvedStage> {
97        if let Some(url) = crate::http_post::parse_http_post_url(stage) {
98            let decision = self.decide("http_post");
99            let st = crate::http_post::HttpPostStage::new(url.to_string(), decision).ok()?;
100            return Some(ResolvedStage::Stateful(Box::new(st)));
101        }
102
103        if let Some(cmd) = crate::exec::parse_exec_cmd(stage) {
104            let decision = self.decide("exec");
105            let st = crate::exec::ExecStage::new(
106                cmd.program.to_string(),
107                cmd.args.into_iter().map(|s| s.to_string()).collect(),
108                decision,
109            )
110            .ok()?;
111            return Some(ResolvedStage::Stateful(Box::new(st)));
112        }
113
114        None
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn stage_families_publish_parameterized_keys() {
124        let families = stage_families();
125        assert_eq!(families.len(), 2);
126        assert_eq!(families[0].key_prefix, STAGE_KEY_HTTP_POST_PREFIX);
127        assert_eq!(families[1].key_prefix, STAGE_KEY_EXEC_PREFIX);
128        assert!(families[0].approval_capabilities.contains(&"http_post"));
129        assert!(families[1].approval_capabilities.contains(&"exec"));
130    }
131}