wrkflw-runtime 0.8.0

Runtime execution environment for wrkflw workflow engine
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
use crate::container::{
    rebase_working_dir_or_error, ContainerError, ContainerOutput, ContainerRuntime,
};
use crate::sandbox::{create_workflow_sandbox_config, Sandbox, SandboxConfig, SandboxError};
use async_trait::async_trait;
use std::path::Path;
use wrkflw_logging;

/// Secure emulation runtime that uses sandboxing for safety
pub struct SecureEmulationRuntime {
    sandbox: Sandbox,
}

impl Default for SecureEmulationRuntime {
    fn default() -> Self {
        Self::new()
    }
}

impl SecureEmulationRuntime {
    /// Create a new secure emulation runtime with default workflow-friendly configuration
    pub fn new() -> Self {
        let config = create_workflow_sandbox_config();
        let sandbox = Sandbox::new(config).expect("Failed to create sandbox");

        wrkflw_logging::info(&format!(
            "{} Initialized secure emulation runtime with sandboxing",
            wrkflw_logging::symbols::LOCK
        ));

        Self { sandbox }
    }

    /// Create a new secure emulation runtime with custom sandbox configuration
    pub fn new_with_config(config: SandboxConfig) -> Result<Self, ContainerError> {
        let sandbox = Sandbox::new(config).map_err(|e| {
            ContainerError::ContainerStart(format!("Failed to create sandbox: {}", e))
        })?;

        wrkflw_logging::info(&format!(
            "{} Initialized secure emulation runtime with custom config",
            wrkflw_logging::symbols::LOCK
        ));

        Ok(Self { sandbox })
    }
}

#[async_trait]
impl ContainerRuntime for SecureEmulationRuntime {
    async fn run_container(
        &self,
        image: &str,
        command: &[&str],
        env_vars: &[(&str, &str)],
        working_dir: &Path,
        volumes: &[(&Path, &Path)],
        entrypoint: Option<&str>,
    ) -> Result<ContainerOutput, ContainerError> {
        if let Some(ep) = entrypoint {
            wrkflw_logging::warning(&format!(
                "Secure emulation mode ignoring entrypoint override '{}' for image '{}'. \
                 Use --runtime docker for full Docker action support.",
                ep, image
            ));
        }

        wrkflw_logging::info(&format!(
            "{} Executing sandboxed command: {} (image: {})",
            wrkflw_logging::symbols::LOCK,
            command.join(" "),
            image
        ));

        // Rebase the container-visible working_dir onto its host-side volume
        // source, matching EmulationRuntime and docker/podman (#88). Without
        // this, `run:` steps and artifact/cache handlers observe different
        // host directories.
        let host_working_dir =
            rebase_working_dir_or_error(working_dir, volumes, "secure_emulation")?;

        // Use sandbox to execute the command safely
        let result = self
            .sandbox
            .execute_command(command, env_vars, &host_working_dir)
            .await;

        match result {
            Ok(output) => {
                wrkflw_logging::info(&format!(
                    "{} Sandboxed command completed successfully",
                    wrkflw_logging::symbols::SUCCESS
                ));
                Ok(output)
            }
            Err(SandboxError::BlockedCommand { command }) => {
                let error_msg = format!(
                    "{} SECURITY BLOCK: Command '{}' is not allowed in secure emulation mode. \
                     This command was blocked for security reasons. \
                     If you need to run this command, please use Docker or Podman mode instead.",
                    wrkflw_logging::symbols::BLOCKED,
                    command
                );
                wrkflw_logging::warning(&error_msg);
                Err(ContainerError::ContainerExecution(error_msg))
            }
            Err(SandboxError::DangerousPattern { pattern }) => {
                let error_msg = format!(
                    "{} SECURITY BLOCK: Dangerous command pattern detected: '{}'. \
                     This command was blocked because it matches a known dangerous pattern. \
                     Please review your workflow for potentially harmful commands.",
                    wrkflw_logging::symbols::BLOCKED,
                    pattern
                );
                wrkflw_logging::warning(&error_msg);
                Err(ContainerError::ContainerExecution(error_msg))
            }
            Err(SandboxError::ExecutionTimeout { seconds }) => {
                let error_msg = format!(
                    "{} Command execution timed out after {} seconds. \
                     Consider optimizing your command or increasing timeout limits.",
                    wrkflw_logging::symbols::WARNING,
                    seconds
                );
                wrkflw_logging::warning(&error_msg);
                Err(ContainerError::ContainerExecution(error_msg))
            }
            Err(SandboxError::PathAccessDenied { path }) => {
                let error_msg = format!(
                    "{} Path access denied: '{}'. \
                     The sandbox restricts file system access for security.",
                    wrkflw_logging::symbols::BLOCKED,
                    path
                );
                wrkflw_logging::warning(&error_msg);
                Err(ContainerError::ContainerExecution(error_msg))
            }
            Err(SandboxError::ResourceLimitExceeded { resource }) => {
                let error_msg = format!(
                    "{} Resource limit exceeded: {}. \
                     Your command used too many system resources.",
                    wrkflw_logging::symbols::WARNING,
                    resource
                );
                wrkflw_logging::warning(&error_msg);
                Err(ContainerError::ContainerExecution(error_msg))
            }
            Err(e) => {
                let error_msg = format!("Sandbox execution failed: {}", e);
                wrkflw_logging::error(&error_msg);
                Err(ContainerError::ContainerExecution(error_msg))
            }
        }
    }

    async fn pull_image(&self, image: &str) -> Result<(), ContainerError> {
        wrkflw_logging::info(&format!(
            "{} Secure emulation: Pretending to pull image {}",
            wrkflw_logging::symbols::LOCK,
            image
        ));
        Ok(())
    }

    async fn build_image(
        &self,
        dockerfile: &Path,
        tag: &str,
        _context_dir: &Path,
    ) -> Result<(), ContainerError> {
        wrkflw_logging::info(&format!(
            "{} Secure emulation: Pretending to build image {} from {}",
            wrkflw_logging::symbols::LOCK,
            tag,
            dockerfile.display()
        ));
        Ok(())
    }

    async fn image_exists(&self, _tag: &str) -> Result<bool, ContainerError> {
        Ok(false)
    }

    async fn prepare_language_environment(
        &self,
        language: &str,
        version: Option<&str>,
        _additional_packages: Option<Vec<String>>,
    ) -> Result<String, ContainerError> {
        // For secure emulation runtime, we'll use a simplified approach
        // that doesn't require building custom images
        let base_image = match language {
            "python" => version.map_or("python:3.11-slim".to_string(), |v| format!("python:{}", v)),
            "node" => version.map_or("node:20-slim".to_string(), |v| format!("node:{}", v)),
            "java" => version.map_or("eclipse-temurin:17-jdk".to_string(), |v| {
                format!("eclipse-temurin:{}", v)
            }),
            "go" => version.map_or("golang:1.21-slim".to_string(), |v| format!("golang:{}", v)),
            "dotnet" => version.map_or("mcr.microsoft.com/dotnet/sdk:7.0".to_string(), |v| {
                format!("mcr.microsoft.com/dotnet/sdk:{}", v)
            }),
            "rust" => version.map_or("rust:latest".to_string(), |v| format!("rust:{}", v)),
            _ => {
                return Err(ContainerError::ContainerStart(format!(
                    "Unsupported language: {}",
                    language
                )))
            }
        };

        // For emulation, we'll just return the base image
        // The actual package installation will be handled during container execution
        Ok(base_image)
    }
}

/// Handle special actions in secure emulation mode
pub async fn handle_special_action_secure(action: &str) -> Result<(), ContainerError> {
    // Extract owner, repo and version from the action
    let action_parts: Vec<&str> = action.split('@').collect();
    let action_name = action_parts[0];
    let action_version = if action_parts.len() > 1 {
        action_parts[1]
    } else {
        "latest"
    };

    wrkflw_logging::info(&format!(
        "{} Processing action in secure mode: {} @ {}",
        wrkflw_logging::symbols::LOCK,
        action_name,
        action_version
    ));

    // In secure mode, we're more restrictive about what actions we allow
    match action_name {
        // Core GitHub actions that are generally safe
        name if name.starts_with("actions/checkout") => {
            wrkflw_logging::info(&format!(
                "{} Checkout action - workspace files are prepared securely",
                wrkflw_logging::symbols::SUCCESS
            ));
        }
        name if name.starts_with("actions/setup-node") => {
            wrkflw_logging::info(&format!(
                "{} Node.js setup - using system Node.js in secure mode",
                wrkflw_logging::symbols::WARNING
            ));
            check_command_available_secure("node", "Node.js", "https://nodejs.org/");
        }
        name if name.starts_with("actions/setup-python") => {
            wrkflw_logging::info(&format!(
                "{} Python setup - using system Python in secure mode",
                wrkflw_logging::symbols::WARNING
            ));
            check_command_available_secure("python", "Python", "https://www.python.org/downloads/");
        }
        name if name.starts_with("actions/setup-java") => {
            wrkflw_logging::info(&format!(
                "{} Java setup - using system Java in secure mode",
                wrkflw_logging::symbols::WARNING
            ));
            check_command_available_secure("java", "Java", "https://adoptium.net/");
        }
        name if name.starts_with("actions/cache") => {
            wrkflw_logging::info(&format!(
                "{} Cache action - caching disabled in secure emulation mode",
                wrkflw_logging::symbols::WARNING
            ));
        }

        // Rust-specific actions
        name if name.starts_with("actions-rs/cargo") => {
            wrkflw_logging::info(&format!(
                "{} Rust cargo action - using system Rust in secure mode",
                wrkflw_logging::symbols::WARNING
            ));
            check_command_available_secure("cargo", "Rust/Cargo", "https://rustup.rs/");
        }
        name if name.starts_with("actions-rs/toolchain") => {
            wrkflw_logging::info(&format!(
                "{} Rust toolchain action - using system Rust in secure mode",
                wrkflw_logging::symbols::WARNING
            ));
            check_command_available_secure("rustc", "Rust", "https://rustup.rs/");
        }
        name if name.starts_with("actions-rs/fmt") => {
            wrkflw_logging::info(&format!(
                "{} Rust formatter action - using system rustfmt in secure mode",
                wrkflw_logging::symbols::WARNING
            ));
            check_command_available_secure("rustfmt", "rustfmt", "rustup component add rustfmt");
        }

        // Potentially dangerous actions that we warn about
        name if name.contains("docker") || name.contains("container") => {
            wrkflw_logging::warning(&format!(
                "{} Docker/container action '{}' is not supported in secure emulation mode. \
                 Use Docker or Podman mode for container actions.",
                wrkflw_logging::symbols::BLOCKED,
                action_name
            ));
        }
        name if name.contains("ssh") || name.contains("deploy") => {
            wrkflw_logging::warning(&format!(
                "{} SSH/deployment action '{}' is restricted in secure emulation mode. \
                 Use Docker or Podman mode for deployment actions.",
                wrkflw_logging::symbols::BLOCKED,
                action_name
            ));
        }

        // Unknown actions
        _ => {
            wrkflw_logging::warning(&format!(
                "{} Unknown action '{}' in secure emulation mode. \
                 Some functionality may be limited or unavailable.",
                wrkflw_logging::symbols::WARNING,
                action_name
            ));
        }
    }

    Ok(())
}

/// Check if a command is available, with security-focused messaging
fn check_command_available_secure(command: &str, name: &str, install_url: &str) {
    use std::process::Command;

    let is_available = Command::new("which")
        .arg(command)
        .output()
        .map(|output| output.status.success())
        .unwrap_or(false);

    if !is_available {
        wrkflw_logging::warning(&format!(
            "🔧 {} is required but not found on the system",
            name
        ));
        wrkflw_logging::info(&format!(
            "To use this action in secure mode, please install {}: {}",
            name, install_url
        ));
        wrkflw_logging::info(&format!(
            "Alternatively, use Docker or Podman mode for automatic {} installation",
            name
        ));
    } else {
        // Try to get version information
        if let Ok(output) = Command::new(command).arg("--version").output() {
            if output.status.success() {
                let version = String::from_utf8_lossy(&output.stdout);
                wrkflw_logging::info(&format!(
                    "{} Using system {} in secure mode: {}",
                    wrkflw_logging::symbols::SUCCESS,
                    name,
                    version.trim()
                ));
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::sandbox::create_strict_sandbox_config;
    use std::path::PathBuf;

    #[tokio::test]
    async fn test_secure_emulation_blocks_dangerous_commands() {
        let config = create_strict_sandbox_config();
        let runtime = SecureEmulationRuntime::new_with_config(config).unwrap();

        // Should block dangerous commands
        let result = runtime
            .run_container(
                "alpine:latest",
                &["rm", "-rf", "/"],
                &[],
                &PathBuf::from("."),
                &[],
                None,
            )
            .await;

        assert!(result.is_err());
        let error_msg = result.unwrap_err().to_string();
        assert!(error_msg.contains("SECURITY BLOCK"));
    }

    #[tokio::test]
    async fn test_secure_emulation_allows_safe_commands() {
        let runtime = SecureEmulationRuntime::new();

        // Should allow safe commands
        let result = runtime
            .run_container(
                "alpine:latest",
                &["echo", "hello world"],
                &[],
                &PathBuf::from("."),
                &[],
                None,
            )
            .await;

        assert!(result.is_ok());
        let output = result.unwrap();
        assert!(output.stdout.contains("hello world"));
        assert_eq!(output.exit_code, 0);
    }

    /// Regression for #88: a container-visible working dir must be rebased
    /// through the `volumes` mapping onto its host counterpart, so commands
    /// run in the caller's workspace rather than a hidden sandbox copy.
    #[cfg(not(target_os = "windows"))]
    #[tokio::test]
    async fn secure_emulation_rebases_container_working_dir_via_volumes() {
        let runtime = SecureEmulationRuntime::new();
        let host_tempdir = tempfile::tempdir().unwrap();

        let host = host_tempdir.path();
        let container = Path::new("/github/workspace");

        // Run `pwd` in the container workspace; with the rebase it should
        // print the host tempdir.
        let result = runtime
            .run_container(
                "alpine:latest",
                &["pwd"],
                &[],
                container,
                &[(host, container)],
                None,
            )
            .await
            .expect("secure_emulation run failed");
        assert_eq!(result.exit_code, 0, "stderr: {}", result.stderr);
        // `pwd` may canonicalize /var → /private/var on macOS, so compare
        // canonical forms.
        let canon_host = host.canonicalize().unwrap();
        let canon_pwd = PathBuf::from(result.stdout.trim()).canonicalize().unwrap();
        assert_eq!(canon_pwd, canon_host);
    }

    #[tokio::test]
    async fn secure_emulation_errors_when_volumes_dont_cover_working_dir() {
        let runtime = SecureEmulationRuntime::new();

        // /github/workspace doesn't exist on host and no volume covers it.
        let result = runtime
            .run_container(
                "alpine:latest",
                &["echo", "nope"],
                &[],
                Path::new("/github/workspace"),
                &[],
                None,
            )
            .await;

        let err = result.expect_err("should error when no volume covers working_dir");
        let msg = err.to_string();
        assert!(
            msg.contains("not covered by any volume mount"),
            "unexpected error: {}",
            msg
        );
    }
}