iron-core 0.1.36

Core AgentIron loop, session state, and tool registry
Documentation
use crate::capability::CapabilityRegistry;
use crate::config::{ApprovalStrategy, Config, EmbeddedPythonConfig};

pub struct RuntimeContextRenderer;

impl RuntimeContextRenderer {
    pub fn render(
        config: &Config,
        capabilities: Option<&CapabilityRegistry>,
        working_dir: &std::path::Path,
        workspace_roots: &[std::path::PathBuf],
        is_git_repo: bool,
        python_exec_available: bool,
    ) -> String {
        let mut ctx = String::new();
        ctx.push_str("<runtime_context>\n");

        ctx.push_str(&format!("Date: {}\n", chrono_date_or_fallback()));
        ctx.push_str(&format!("Platform: {}\n", std::env::consts::OS));
        ctx.push_str(&format!("Working directory: {}\n", working_dir.display()));
        for root in workspace_roots {
            ctx.push_str(&format!("Workspace root: {}\n", root.display()));
        }
        if is_git_repo {
            ctx.push_str("Git repository: yes\n");
        }

        ctx.push_str(&format!(
            "Approval strategy: {}\n",
            approval_strategy_label(config.default_approval_strategy)
        ));

        if !config.prompt_composition.protected_resources.is_empty() {
            ctx.push_str("\nProtected resources:\n");
            ctx.push_str("The following resources must never be read or modified, whether through direct tool use or scripted tool use (e.g. python_exec):\n");
            for res in &config.prompt_composition.protected_resources {
                ctx.push_str(&format!("- {}\n", res));
            }
        }

        if let Some(caps) = capabilities {
            if !caps.is_empty() {
                ctx.push_str("\nCapabilities:\n");
                for (_, desc) in caps.iter() {
                    let perm = if desc.requires_permission {
                        " (may require approval)"
                    } else {
                        ""
                    };
                    ctx.push_str(&format!("- {}{}: {}\n", desc.name, perm, desc.description));
                }
            }
        }

        if config.embedded_python.enabled && python_exec_available {
            ctx.push_str(&render_python_context(&config.embedded_python));
        }

        ctx.push_str("</runtime_context>");
        ctx
    }
}

fn approval_strategy_label(strategy: ApprovalStrategy) -> &'static str {
    match strategy {
        ApprovalStrategy::Always => "always",
        ApprovalStrategy::Never => "never",
        ApprovalStrategy::PerTool => "per-tool",
        ApprovalStrategy::AutoApprove => "auto-approve",
    }
}

fn render_python_context(config: &EmbeddedPythonConfig) -> String {
    let mut s = String::new();
    s.push_str("\nEmbedded Python (python_exec):\n");
    s.push_str("Status: enabled\n");
    s.push_str("Preferred uses: deterministic computation, tool orchestration, safe parallelization of independent tasks.\n");
    s.push_str("Tool access: the script receives `tools`, a namespace derived from the visible runtime tool catalog. Prefer `await tools.<tool>(payload)` for Python-safe aliases, use `await tools.call(name, payload)` for raw-name fallback, and keep `iron_call(name, args)` only as a low-level escape hatch.\n");
    s.push_str("Sandbox boundary: direct OS, filesystem, and network access from Python is unavailable. For host interactions (read/write files, execute commands, network requests), use the tools namespace: `await tools.<alias>(payload)` or `await tools.call(name, payload)`. Direct use of pathlib, open, os, and similar APIs for host access is not supported.\n");
    s.push_str("Restrictions:\n");
    s.push_str("- No package installation (pip is unavailable)\n");
    s.push_str("- Do not assume arbitrary third-party libraries are available\n");
    s.push_str("- Do not rely on classes unless explicitly supported\n");
    s.push_str("- Do not assume full CPython compatibility\n");
    s.push_str(&format!(
        "Limits: timeout {}s, source {} bytes, result {} bytes, child calls {}\n",
        config.max_script_timeout_secs,
        config.max_source_bytes,
        config.max_result_bytes,
        config.max_child_calls,
    ));
    s
}

fn chrono_date_or_fallback() -> String {
    match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
        Ok(d) => {
            let days_since_epoch = d.as_secs() / 86400;
            let year = 1970 + (days_since_epoch / 365) as i32;
            format!("{} (approx)", year)
        }
        Err(_) => "unknown".to_string(),
    }
}