Skip to main content

tryaudex_core/
chain.rs

1use serde::{Deserialize, Serialize};
2
3/// Ambient credential environment variables that must be stripped from any
4/// subprocess audex spawns with scoped credentials. Leaving these in the
5/// child environment would let the subprocess fall back to the operator's
6/// personal credentials via AWS/GCP/Azure SDK precedence rules, bypassing
7/// the scoped session entirely.
8///
9/// R6-M28: `chain`, `learn`, `run`, and `mcp` each kept their own inline
10/// list and diverged. The missing GCP entries
11/// (`GOOGLE_OAUTH_ACCESS_TOKEN`, `GOOGLE_CLOUD_PROJECT`,
12/// `CLOUDSDK_CORE_PROJECT`) meant a subprocess could still pick up an
13/// ambient OAuth token or default project even when the audex session
14/// targeted a different SA. Centralise the canonical list here so every
15/// spawn path stays in sync.
16pub const AMBIENT_CRED_ENV_VARS: &[&str] = &[
17    // AWS
18    "AWS_PROFILE",
19    "AWS_DEFAULT_PROFILE",
20    "AWS_CONFIG_FILE",
21    "AWS_SHARED_CREDENTIALS_FILE",
22    "AWS_WEB_IDENTITY_TOKEN_FILE",
23    "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
24    "AWS_CONTAINER_CREDENTIALS_FULL_URI",
25    // GCP
26    "GOOGLE_APPLICATION_CREDENTIALS",
27    "GOOGLE_OAUTH_ACCESS_TOKEN",
28    "GOOGLE_CLOUD_PROJECT",
29    "CLOUDSDK_AUTH_ACCESS_TOKEN",
30    "CLOUDSDK_CONFIG",
31    "CLOUDSDK_CORE_PROJECT",
32    // Azure
33    "AZURE_CLIENT_SECRET",
34    "AZURE_TENANT_ID",
35];
36
37/// A single step in a multi-step session chain.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ChainStep {
40    /// IAM actions allowed for this step (e.g. "s3:GetObject")
41    pub allow: String,
42    /// Optional resource restriction
43    pub resource: Option<String>,
44    /// Optional named profile
45    pub profile: Option<String>,
46    /// TTL for this step's credentials (defaults to parent TTL)
47    pub ttl: Option<String>,
48    /// Command to run in this step
49    pub command: Vec<String>,
50}
51
52/// Result of executing a single chain step.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct StepResult {
55    pub step_index: usize,
56    pub allow: String,
57    pub exit_code: i32,
58    pub stdout: String,
59    pub stderr: String,
60}
61
62/// Full result of a chain execution.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct ChainResult {
65    pub steps: Vec<StepResult>,
66    pub total_steps: usize,
67    pub completed_steps: usize,
68    pub success: bool,
69}
70
71impl ChainResult {
72    pub fn new(total: usize) -> Self {
73        Self {
74            steps: Vec::new(),
75            total_steps: total,
76            completed_steps: 0,
77            success: true,
78        }
79    }
80
81    pub fn add_step(&mut self, result: StepResult) {
82        if result.exit_code != 0 {
83            self.success = false;
84        }
85        self.completed_steps += 1;
86        self.steps.push(result);
87    }
88}
89
90/// Parse a step specification string.
91/// Format: "allow:command" or just "allow" (command comes from CLI args).
92pub fn parse_step_spec(spec: &str) -> ChainStep {
93    ChainStep {
94        allow: spec.to_string(),
95        resource: None,
96        profile: None,
97        ttl: None,
98        command: Vec::new(),
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn test_parse_step_spec() {
108        let step = parse_step_spec("s3:GetObject,s3:ListBucket");
109        assert_eq!(step.allow, "s3:GetObject,s3:ListBucket");
110        assert!(step.resource.is_none());
111        assert!(step.command.is_empty());
112    }
113
114    #[test]
115    fn test_chain_result_success() {
116        let mut result = ChainResult::new(2);
117        result.add_step(StepResult {
118            step_index: 0,
119            allow: "s3:GetObject".to_string(),
120            exit_code: 0,
121            stdout: "ok".to_string(),
122            stderr: String::new(),
123        });
124        result.add_step(StepResult {
125            step_index: 1,
126            allow: "lambda:InvokeFunction".to_string(),
127            exit_code: 0,
128            stdout: "ok".to_string(),
129            stderr: String::new(),
130        });
131        assert!(result.success);
132        assert_eq!(result.completed_steps, 2);
133    }
134
135    #[test]
136    fn test_chain_result_failure_stops() {
137        let mut result = ChainResult::new(3);
138        result.add_step(StepResult {
139            step_index: 0,
140            allow: "s3:GetObject".to_string(),
141            exit_code: 0,
142            stdout: "ok".to_string(),
143            stderr: String::new(),
144        });
145        result.add_step(StepResult {
146            step_index: 1,
147            allow: "lambda:InvokeFunction".to_string(),
148            exit_code: 1,
149            stdout: String::new(),
150            stderr: "access denied".to_string(),
151        });
152        assert!(!result.success);
153        assert_eq!(result.completed_steps, 2);
154    }
155
156    #[test]
157    fn test_chain_result_serializable() {
158        let result = ChainResult::new(1);
159        let json = serde_json::to_string(&result).unwrap();
160        assert!(json.contains("\"total_steps\":1"));
161    }
162}