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,3 @@
pub mod workflow;
pub use workflow::{StepMeta, StepOutputs, WorkflowInputs};

View File

@@ -0,0 +1,52 @@
use std::collections::HashMap;
use deno_core::op2;
use deno_core::OpState;
/// Workflow data available to the script via `inputs()`.
pub struct WorkflowInputs {
pub data: serde_json::Value,
}
/// Accumulates key/value outputs set by the script via `output(key, value)`.
pub struct StepOutputs {
pub map: HashMap<String, serde_json::Value>,
}
/// Metadata about the currently executing step.
pub struct StepMeta {
pub name: String,
}
/// Returns the workflow input data to JavaScript.
#[op2]
#[serde]
pub fn op_inputs(state: &mut OpState) -> serde_json::Value {
let inputs = state.borrow::<WorkflowInputs>();
inputs.data.clone()
}
/// Stores a key/value pair in the step outputs.
#[op2]
pub fn op_output(
state: &mut OpState,
#[string] key: String,
#[serde] value: serde_json::Value,
) {
let outputs = state.borrow_mut::<StepOutputs>();
outputs.map.insert(key, value);
}
/// Logs a message via the tracing crate.
#[op2(fast)]
pub fn op_log(state: &mut OpState, #[string] msg: String) {
let name = state.borrow::<StepMeta>().name.clone();
tracing::info!(step = %name, "{}", msg);
}
deno_core::extension!(
wfe_ops,
ops = [op_inputs, op_output, op_log],
esm_entry_point = "ext:wfe/bootstrap.js",
esm = ["ext:wfe/bootstrap.js" = "src/executors/deno/js/bootstrap.js"],
);