execgo-runtime 1.0.0-b1

Adaptive data-plane runtime for ExecGo with HTTP API, CLI, capability negotiation, and local resource ledger
Documentation
use crate::{
    error::{AppError, AppResult},
    types::{
        CapabilityMode, ExecutionPlan, NamespaceConfig, ResourceEnforcementPlan,
        RuntimeCapabilities, SandboxProfile, SubmitTaskRequest,
    },
};

pub fn resolve_execution_plan(
    request: &SubmitTaskRequest,
    capabilities: &RuntimeCapabilities,
    default_mode: CapabilityMode,
) -> AppResult<ExecutionPlan> {
    let strict = effective_capability_mode(request, default_mode) == CapabilityMode::Strict
        || request
            .control_context
            .as_ref()
            .map(|context| context.requires_strict_sandbox)
            .unwrap_or(false);

    let mut degraded = false;
    let mut fallback_reasons = Vec::new();
    let mut effective_sandbox = request.sandbox.clone();

    if matches!(effective_sandbox.profile, SandboxProfile::LinuxSandbox)
        && !capabilities.sandbox.linux_sandbox
    {
        if strict {
            return Err(AppError::UnsupportedCapability(
                "sandbox.profile=linux_sandbox is unavailable on this runtime".into(),
            ));
        }
        degraded = true;
        fallback_reasons
            .push("linux_sandbox is unavailable; falling back to process sandbox".into());
        effective_sandbox.profile = SandboxProfile::Process;
        effective_sandbox.chroot = false;
        effective_sandbox.rootfs = None;
        effective_sandbox.namespaces = None;
    }

    if effective_sandbox.chroot && !capabilities.sandbox.chroot {
        if strict {
            return Err(AppError::UnsupportedCapability(
                "sandbox.chroot is unavailable on this runtime".into(),
            ));
        }
        degraded = true;
        fallback_reasons.push("chroot is unavailable; running without chroot".into());
        effective_sandbox.chroot = false;
        effective_sandbox.rootfs = None;
    }

    if matches!(effective_sandbox.profile, SandboxProfile::LinuxSandbox) {
        let requested_namespaces = effective_sandbox.effective_namespaces();
        let adjusted = NamespaceConfig {
            mount: namespace_or_fallback(
                requested_namespaces.mount,
                capabilities.sandbox.namespaces.mount,
                strict,
                &mut degraded,
                &mut fallback_reasons,
                "mount",
            )?,
            pid: namespace_or_fallback(
                requested_namespaces.pid,
                capabilities.sandbox.namespaces.pid,
                strict,
                &mut degraded,
                &mut fallback_reasons,
                "pid",
            )?,
            uts: namespace_or_fallback(
                requested_namespaces.uts,
                capabilities.sandbox.namespaces.uts,
                strict,
                &mut degraded,
                &mut fallback_reasons,
                "uts",
            )?,
            ipc: namespace_or_fallback(
                requested_namespaces.ipc,
                capabilities.sandbox.namespaces.ipc,
                strict,
                &mut degraded,
                &mut fallback_reasons,
                "ipc",
            )?,
            net: namespace_or_fallback(
                requested_namespaces.net,
                capabilities.sandbox.namespaces.net,
                strict,
                &mut degraded,
                &mut fallback_reasons,
                "net",
            )?,
        };
        effective_sandbox.namespaces = Some(adjusted);
    }

    let cgroup_enforced = matches!(effective_sandbox.profile, SandboxProfile::LinuxSandbox)
        && capabilities.resources.cgroup_writable;
    let cpu_time_enforced =
        request.limits.cpu_time_sec.is_none() || capabilities.resources.rlimit_cpu;
    let memory_enforced =
        request.limits.memory_bytes.is_none() || capabilities.resources.rlimit_memory;
    let pids_enforced =
        request.limits.pids_max.is_none() || (cgroup_enforced && capabilities.resources.pids_limit);
    let oom_detection = request.limits.memory_bytes.is_some()
        && cgroup_enforced
        && capabilities.resources.oom_detection;

    if request.limits.cpu_time_sec.is_some() && !cpu_time_enforced {
        if strict {
            return Err(AppError::UnsupportedCapability(
                "cpu_time_sec enforcement is unavailable on this runtime".into(),
            ));
        }
        degraded = true;
        fallback_reasons.push("cpu_time_sec enforcement is unavailable".into());
    }

    if request.limits.memory_bytes.is_some() && !memory_enforced {
        if strict {
            return Err(AppError::UnsupportedCapability(
                "memory_bytes enforcement is unavailable on this runtime".into(),
            ));
        }
        degraded = true;
        fallback_reasons.push("memory_bytes enforcement is unavailable".into());
    }

    if request.limits.pids_max.is_some() && !pids_enforced {
        if strict {
            return Err(AppError::UnsupportedCapability(
                "pids_max enforcement requires writable cgroup support".into(),
            ));
        }
        degraded = true;
        fallback_reasons.push("pids_max enforcement requires writable cgroup support".into());
    }

    Ok(ExecutionPlan {
        capability_mode: effective_capability_mode(request, default_mode),
        requested_sandbox: request.sandbox.clone(),
        effective_sandbox,
        resource_enforcement: ResourceEnforcementPlan {
            wall_time_ms: request.limits.wall_time_ms,
            cpu_time_sec: request.limits.cpu_time_sec,
            cpu_time_enforced: request.limits.cpu_time_sec.is_some() && cpu_time_enforced,
            memory_bytes: request.limits.memory_bytes,
            memory_enforced: request.limits.memory_bytes.is_some() && memory_enforced,
            pids_max: request.limits.pids_max,
            pids_enforced: request.limits.pids_max.is_some() && pids_enforced,
            cgroup_enforced,
            oom_detection,
        },
        degraded,
        fallback_reasons,
        capability_warnings: capabilities.warnings.clone(),
    })
}

pub fn effective_capability_mode(
    request: &SubmitTaskRequest,
    default_mode: CapabilityMode,
) -> CapabilityMode {
    request
        .policy
        .as_ref()
        .map(|policy| policy.capability_mode)
        .unwrap_or(default_mode)
}

fn namespace_or_fallback(
    requested: bool,
    supported: bool,
    strict: bool,
    degraded: &mut bool,
    fallback_reasons: &mut Vec<String>,
    namespace: &str,
) -> AppResult<bool> {
    if !requested {
        return Ok(false);
    }
    if supported {
        return Ok(true);
    }
    if strict {
        return Err(AppError::UnsupportedCapability(format!(
            "{namespace} namespace is unavailable on this runtime"
        )));
    }
    *degraded = true;
    fallback_reasons.push(format!(
        "{namespace} namespace is unavailable; running without it"
    ));
    Ok(false)
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;

    use chrono::Utc;

    use super::*;
    use crate::types::{
        ControlContext, ExecutionCapabilities, ExecutionKind, ExecutionSpec, NamespaceCapabilities,
        ResourceCapabilities, ResourceCapacity, ResourceLimits, RuntimePlatform,
        SandboxCapabilities, SandboxPolicy, StorageCapabilities, TaskPolicy,
    };

    fn capabilities(linux_sandbox: bool, cgroup_writable: bool) -> RuntimeCapabilities {
        RuntimeCapabilities {
            runtime_id: "test".into(),
            snapshot_version: "v1".into(),
            collected_at: Utc::now(),
            platform: RuntimePlatform {
                os: "test".into(),
                arch: "test".into(),
                containerized: false,
                kubernetes: false,
            },
            execution: ExecutionCapabilities {
                command: true,
                script: true,
                process_group: true,
            },
            sandbox: SandboxCapabilities {
                process: true,
                linux_sandbox,
                chroot: linux_sandbox,
                namespaces: NamespaceCapabilities {
                    mount: linux_sandbox,
                    pid: linux_sandbox,
                    uts: linux_sandbox,
                    ipc: linux_sandbox,
                    net: linux_sandbox,
                },
            },
            storage: StorageCapabilities {
                data_dir_writable: true,
            },
            resources: ResourceCapabilities {
                rlimit_cpu: true,
                rlimit_memory: true,
                cgroup_v2: cgroup_writable,
                cgroup_writable,
                memory_limit: true,
                pids_limit: cgroup_writable,
                oom_detection: cgroup_writable,
                cpu_quota: false,
                ledger: true,
                capacity: ResourceCapacity {
                    task_slots: 4,
                    memory_bytes: Some(1024),
                    pids: Some(64),
                },
            },
            stable_semantics: vec![],
            enhanced_semantics: vec![],
            warnings: vec![],
            degraded: false,
            overrides: BTreeMap::new(),
        }
    }

    fn request() -> SubmitTaskRequest {
        SubmitTaskRequest {
            task_id: None,
            execution: ExecutionSpec {
                kind: ExecutionKind::Command,
                program: Some("/bin/echo".into()),
                args: vec!["ok".into()],
                script: None,
                interpreter: None,
                env: Default::default(),
            },
            limits: ResourceLimits::default(),
            sandbox: SandboxPolicy::default(),
            policy: None,
            control_context: None,
            metadata: BTreeMap::new(),
        }
    }

    #[test]
    fn process_request_stays_stable() {
        let plan = resolve_execution_plan(
            &request(),
            &capabilities(false, false),
            CapabilityMode::Adaptive,
        )
        .expect("plan");
        assert!(!plan.degraded);
        assert_eq!(plan.effective_sandbox.profile, SandboxProfile::Process);
    }

    #[test]
    fn adaptive_linux_sandbox_falls_back() {
        let mut request = request();
        request.sandbox.profile = SandboxProfile::LinuxSandbox;

        let plan = resolve_execution_plan(
            &request,
            &capabilities(false, false),
            CapabilityMode::Adaptive,
        )
        .expect("plan");
        assert!(plan.degraded);
        assert_eq!(plan.effective_sandbox.profile, SandboxProfile::Process);
    }

    #[test]
    fn strict_linux_sandbox_rejects() {
        let mut request = request();
        request.sandbox.profile = SandboxProfile::LinuxSandbox;
        request.policy = Some(TaskPolicy {
            capability_mode: CapabilityMode::Strict,
        });

        let err = resolve_execution_plan(
            &request,
            &capabilities(false, false),
            CapabilityMode::Adaptive,
        )
        .expect_err("strict should reject");
        assert!(matches!(err, AppError::UnsupportedCapability(_)));
    }

    #[test]
    fn control_context_can_enforce_strict_sandbox() {
        let mut request = request();
        request.sandbox.profile = SandboxProfile::LinuxSandbox;
        request.control_context = Some(ControlContext {
            requires_strict_sandbox: true,
            ..ControlContext::default()
        });

        let err = resolve_execution_plan(
            &request,
            &capabilities(false, false),
            CapabilityMode::Adaptive,
        )
        .expect_err("control context should reject");
        assert!(matches!(err, AppError::UnsupportedCapability(_)));
    }
}