feat(wfe-yaml): add deno_core JS/TS executor with sandboxed permissions

Secure JavaScript/TypeScript execution in workflow steps via deno_core,
behind the `deno` feature flag.

Security features:
- Per-step permission system: net host allowlist, filesystem read/write
  path restrictions, env var allowlist, subprocess spawn control
- V8 heap limits (64MB default) prevent memory exhaustion
- Execution timeout with V8 isolate termination for sync infinite loops
- Path traversal detection blocks ../ escape attempts
- Dynamic import rejection unless explicitly enabled

Workflow I/O ops:
- inputs() — read workflow data as JSON
- output(key, value) — set step outputs
- log(message) — structured tracing

Architecture:
- JsRuntime runs on dedicated thread (V8 is !Send)
- PermissionChecker enforced on every I/O op via OpState
- DenoStep implements StepBody, integrates with existing compiler
- Step type dispatch: "shell" or "deno" in YAML

34 new tests (12 permission unit, 3 config, 2 runtime, 18 integration).
This commit is contained in:
2026-03-25 22:32:07 +00:00
parent ce68e4beed
commit 6fec7dbab5
15 changed files with 1127 additions and 66 deletions

View File

@@ -0,0 +1,81 @@
use std::collections::HashMap;
use deno_core::JsRuntime;
use deno_core::RuntimeOptions;
use wfe_core::WfeError;
use super::config::DenoConfig;
use super::ops::workflow::{wfe_ops, StepMeta, StepOutputs, WorkflowInputs};
use super::permissions::PermissionChecker;
/// Create a configured `JsRuntime` for executing a workflow step script.
pub fn create_runtime(
config: &DenoConfig,
workflow_data: serde_json::Value,
step_name: &str,
) -> Result<JsRuntime, WfeError> {
let ext = wfe_ops::init();
let runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![ext],
..Default::default()
});
// Populate OpState with our workflow types.
{
let state = runtime.op_state();
let mut state = state.borrow_mut();
state.put(WorkflowInputs {
data: workflow_data,
});
state.put(StepOutputs {
map: HashMap::new(),
});
state.put(StepMeta {
name: step_name.to_string(),
});
state.put(PermissionChecker::from_config(&config.permissions));
}
Ok(runtime)
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::config::DenoPermissions;
#[test]
fn create_runtime_succeeds() {
let config = DenoConfig {
script: Some("1+1".to_string()),
file: None,
permissions: DenoPermissions::default(),
modules: vec![],
env: HashMap::new(),
timeout_ms: None,
};
let runtime = create_runtime(&config, serde_json::json!({}), "test-step");
assert!(runtime.is_ok());
}
#[test]
fn create_runtime_has_op_state() {
let config = DenoConfig {
script: None,
file: None,
permissions: DenoPermissions::default(),
modules: vec![],
env: HashMap::new(),
timeout_ms: None,
};
let runtime =
create_runtime(&config, serde_json::json!({"key": "val"}), "my-step").unwrap();
let state = runtime.op_state();
let state = state.borrow();
let inputs = state.borrow::<WorkflowInputs>();
assert_eq!(inputs.data, serde_json::json!({"key": "val"}));
let meta = state.borrow::<StepMeta>();
assert_eq!(meta.name, "my-step");
}
}