runx-runtime 0.6.19

Native Rust runtime for local runx execution, adapters, harness replay, receipts, and sandboxing.
Documentation
use super::{
    InlineHarnessReport, SkillRunError, SkillRunOverrides, execute_skill_run_with_overrides,
};

use std::collections::BTreeMap;
use std::path::Path;

use runx_contracts::{JsonObject, JsonValue};
use runx_parser::{HarnessCallerFixture, RunnerHarnessCase, SkillRunnerManifest};

use crate::RuntimeError;
use crate::effects::RuntimeEffectRegistry;
use crate::execution::orchestrator::SkillRunRequest;

use super::runner_manifest::{load_runner_manifest, resolve_skill_dir, selected_runner};

/// Run a skill's declared inline harness and summarize it. Each declared case is
/// run through the same path as `runx skill` (so a graph that blocks on an agent
/// step yields `needs_agent`, exactly as a real run would), with the case's
/// runner selected and its caller answers/approvals seeded for a single pass.
/// A skill with no declared harness is `not_declared` (not a failure). The
/// run is `passed` only when every case meets its declared expectation.
pub(crate) fn run_inline_harness_with_effects(
    skill_path: &Path,
    receipt_dir: Option<&Path>,
    env: Option<&BTreeMap<String, String>>,
    effects: &RuntimeEffectRegistry,
) -> Result<InlineHarnessReport, SkillRunError> {
    let skill_dir = resolve_skill_dir(skill_path)?;
    let manifest = load_runner_manifest(&skill_dir)?;
    let Some(harness) = manifest.harness.as_ref() else {
        return Ok(InlineHarnessReport::not_declared());
    };
    if harness.cases.is_empty() {
        return Ok(InlineHarnessReport::not_declared());
    }

    let cwd = std::env::current_dir()
        .map_err(|source| RuntimeError::io("resolving cwd for inline harness", source))?;

    let mut assertion_errors = Vec::new();
    let mut case_names = Vec::with_capacity(harness.cases.len());
    let mut receipt_ids = Vec::new();
    let mut graph_case_count = 0;

    for case in &harness.cases {
        case_names.push(case.name.clone());
        let outcome =
            run_inline_harness_case(&skill_dir, receipt_dir, env, &manifest, case, &cwd, effects);
        if outcome.is_graph {
            graph_case_count += 1;
        }
        if let Some(receipt_id) = outcome.receipt_id {
            receipt_ids.push(receipt_id);
        }
        if let Some(error) = outcome.assertion_error {
            assertion_errors.push(error);
        }
    }

    let status = if assertion_errors.is_empty() {
        "passed"
    } else {
        "failed"
    };
    Ok(InlineHarnessReport {
        assertion_error_count: assertion_errors.len(),
        status,
        case_count: harness.cases.len(),
        assertion_errors,
        case_names,
        receipt_ids,
        graph_case_count,
    })
}

struct InlineHarnessCaseOutcome {
    is_graph: bool,
    receipt_id: Option<String>,
    assertion_error: Option<String>,
}

fn run_inline_harness_case(
    skill_dir: &Path,
    receipt_dir: Option<&Path>,
    env: Option<&BTreeMap<String, String>>,
    manifest: &SkillRunnerManifest,
    case: &RunnerHarnessCase,
    cwd: &Path,
    effects: &RuntimeEffectRegistry,
) -> InlineHarnessCaseOutcome {
    let is_graph = match selected_runner(manifest, case.runner.as_deref()) {
        Ok(runner) => runner.source.source_type == runx_parser::SourceKind::Graph,
        Err(error) => return inline_harness_case_error(&case.name, error),
    };
    let request = inline_harness_case_request(skill_dir, receipt_dir, env, case, cwd);
    let overrides = SkillRunOverrides {
        runner: case.runner.clone(),
        seeded_answers: seeded_answers_from_caller(&case.caller),
    };
    match execute_skill_run_with_overrides(&request, &overrides, effects) {
        Ok(output) => InlineHarnessCaseOutcome {
            is_graph,
            receipt_id: receipt_id_from_output(&output),
            assertion_error: inline_harness_expectation_error(case, &output),
        },
        Err(error) => InlineHarnessCaseOutcome {
            is_graph,
            receipt_id: None,
            assertion_error: Some(format!("{}: {error}", case.name)),
        },
    }
}

fn inline_harness_case_request(
    skill_dir: &Path,
    receipt_dir: Option<&Path>,
    env: Option<&BTreeMap<String, String>>,
    case: &RunnerHarnessCase,
    cwd: &Path,
) -> SkillRunRequest {
    let mut env: BTreeMap<String, String> =
        env.cloned().unwrap_or_else(|| std::env::vars().collect());
    env.extend(case.env.clone());
    SkillRunRequest {
        skill_path: skill_dir.to_path_buf(),
        receipt_dir: receipt_dir.map(Path::to_path_buf),
        run_id: None,
        answers_path: None,
        inputs: case.inputs.clone(),
        env,
        cwd: cwd.to_path_buf(),
        local_credential: None,
    }
}

fn inline_harness_case_error(
    case_name: &str,
    error: impl std::fmt::Display,
) -> InlineHarnessCaseOutcome {
    InlineHarnessCaseOutcome {
        is_graph: false,
        receipt_id: None,
        assertion_error: Some(format!("{case_name}: {error}")),
    }
}

fn receipt_id_from_output(output: &JsonValue) -> Option<String> {
    output
        .as_object()
        .and_then(|object| object.get("receipt_id"))
        .and_then(JsonValue::as_str)
        .map(str::to_owned)
}

fn inline_harness_expectation_error(
    case: &RunnerHarnessCase,
    output: &JsonValue,
) -> Option<String> {
    let expected = case.expect.status.as_deref()?;
    let actual = inline_harness_actual_status(output);
    (actual != expected).then(|| format!("{}: expected status {expected}, got {actual}", case.name))
}

// Merge a harness case's caller answers + approvals into one map keyed by
// resolution request id, the shape the seeded agent/graph answer lookup expects.
// Approvals are recorded as booleans under their gate id.
fn seeded_answers_from_caller(caller: &HarnessCallerFixture) -> Option<JsonObject> {
    let mut merged = caller.answers.clone().unwrap_or_default();
    if let Some(approvals) = &caller.approvals {
        for (gate, approved) in approvals {
            merged
                .entry(gate.clone())
                .or_insert_with(|| JsonValue::Bool(*approved));
        }
    }
    if merged.is_empty() {
        None
    } else {
        Some(merged)
    }
}

// Map an `execute_skill_run` output onto the harness status vocabulary
// (sealed/failure/needs_agent/policy_denied). A pending run is needs_agent; a
// terminal run is derived from its closure disposition so the mapping matches
// the standalone harness `status_from_disposition`.
fn inline_harness_actual_status(output: &JsonValue) -> &'static str {
    let Some(object) = output.as_object() else {
        return "sealed";
    };
    if object.get("status").and_then(JsonValue::as_str) == Some("needs_agent") {
        return "needs_agent";
    }
    let disposition = object
        .get("closure")
        .and_then(JsonValue::as_object)
        .and_then(|closure| closure.get("disposition"))
        .and_then(JsonValue::as_str);
    match disposition {
        Some("deferred") => "needs_agent",
        Some("blocked") => "policy_denied",
        Some("declined" | "failed" | "killed" | "timed_out" | "superseded") => "failure",
        _ => "sealed",
    }
}