wasmrun 0.20.0

A WebAssembly Runtime
//! Agent mode: REST API request/response types.

use crate::agent::limits::LimitsOverride;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

// ── Requests ──────────────────────────────────────────────────────────

/// Optional body for `POST /sessions`. Lets a caller override the server's
/// default resource limits for the new session. Absent fields use defaults.
#[derive(Deserialize, Default)]
pub struct CreateSessionRequest {
    pub limits: Option<LimitsOverride>,
}

#[derive(Deserialize)]
pub struct ExecRequest {
    /// Path to a pre-compiled .wasm file (relative to session root).
    pub wasm_path: Option<String>,
    /// Source code to execute via a language runtime (alternative to wasm_path).
    pub source: Option<String>,
    /// Multi-file project: map of filename → file content. Use with `entry`.
    /// Files are written to the session root before execution and visible
    /// to the runtime (e.g. for `require()` of sibling files).
    pub files: Option<HashMap<String, String>>,
    /// Entry filename for a multi-file project (must be a key in `files`).
    pub entry: Option<String>,
    /// Language for source execution: "javascript", "js", or "nodejs".
    pub language: Option<String>,
    /// Shell command line to execute via the built-in shell emulator.
    /// Supports pipes (`|`), redirection (`>`, `>>`, `<`), and sequencing
    /// (`&&`, `;`) with built-ins for common file/env operations.
    /// Takes precedence over `files`/`source`/`wasm_path` when present.
    pub command: Option<String>,
    pub function: Option<String>,
    #[serde(default)]
    pub args: Vec<String>,
    pub timeout: Option<u64>,
    pub env: Option<HashMap<String, String>>,
}

#[derive(Deserialize)]
pub struct WriteFileRequest {
    pub path: String,
    pub content: String,
}

// ── Responses ─────────────────────────────────────────────────────────

#[derive(Debug, Serialize)]
pub struct CreateSessionResponse {
    pub session_id: String,
    pub created_at: String,
}

#[derive(Debug, Serialize)]
pub struct SessionStatusResponse {
    pub session_id: String,
    pub state: String,
    pub created_at_elapsed_ms: u64,
    pub last_accessed_elapsed_ms: u64,
    pub timeout_secs: u64,
}

#[derive(Debug, Serialize)]
pub struct ExecResponse {
    pub stdout: String,
    pub stderr: String,
    pub exit_code: i32,
    pub duration_ms: u64,
    /// True when captured output was dropped because the output cap was hit.
    /// Omitted from JSON when false to keep the common response shape clean.
    #[serde(skip_serializing_if = "std::ops::Not::not")]
    pub output_truncated: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct ReadFileResponse {
    pub path: String,
    pub content: String,
}

#[derive(Debug, Serialize)]
pub struct ListFilesResponse {
    pub path: String,
    pub entries: Vec<FileEntry>,
}

#[derive(Debug, Serialize)]
pub struct FileEntry {
    pub name: String,
    pub is_dir: bool,
    pub size: u64,
}

#[derive(Debug, Serialize)]
pub struct EnvVarsResponse {
    pub env: HashMap<String, String>,
}

#[derive(Debug, Serialize)]
pub struct MessageResponse {
    pub message: String,
}

#[derive(Debug, Serialize)]
pub struct ErrorResponse {
    pub error: String,
    pub code: u16,
}

// ── API Error ─────────────────────────────────────────────────────────

#[derive(Debug)]
pub enum ApiError {
    SessionNotFound(String),
    SessionExpired(String),
    MaxSessions(usize),
    BadRequest(String),
    /// Authentication required but missing/malformed/unknown API key.
    Unauthorized(String),
    NotFound(String),
    /// Request body exceeded the configured size cap. Carries the limit (bytes).
    PayloadTooLarge(usize),
    /// Too many concurrent executions in flight. Carries the configured cap.
    TooManyRequests(usize),
    /// A per-tenant rate limit was exceeded (session count, concurrent exec, or
    /// requests/min). Carries a human-readable reason.
    RateLimited(String),
    #[allow(dead_code)] // TODO: Used when exec timeout triggers API-level error
    Timeout,
    Internal(String),
}

impl ApiError {
    pub fn status_code(&self) -> u16 {
        match self {
            ApiError::SessionNotFound(_) | ApiError::NotFound(_) => 404,
            ApiError::SessionExpired(_) => 410,
            ApiError::MaxSessions(_) | ApiError::TooManyRequests(_) | ApiError::RateLimited(_) => {
                429
            }
            ApiError::BadRequest(_) => 400,
            ApiError::Unauthorized(_) => 401,
            ApiError::PayloadTooLarge(_) => 413,
            ApiError::Timeout => 408,
            ApiError::Internal(_) => 500,
        }
    }

    pub fn to_error_response(&self) -> ErrorResponse {
        ErrorResponse {
            error: self.to_string(),
            code: self.status_code(),
        }
    }
}

impl std::fmt::Display for ApiError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ApiError::SessionNotFound(id) => write!(f, "Session not found: {id}"),
            ApiError::SessionExpired(id) => write!(f, "Session expired: {id}"),
            ApiError::MaxSessions(max) => write!(f, "Maximum sessions reached: {max}"),
            ApiError::BadRequest(msg) => write!(f, "Bad request: {msg}"),
            ApiError::Unauthorized(msg) => write!(f, "Unauthorized: {msg}"),
            ApiError::NotFound(msg) => write!(f, "Not found: {msg}"),
            ApiError::PayloadTooLarge(max) => {
                write!(f, "Request body too large: exceeds {max} byte limit")
            }
            ApiError::TooManyRequests(max) => {
                write!(f, "Too many concurrent executions: limit is {max}")
            }
            ApiError::RateLimited(reason) => write!(f, "Rate limit exceeded: {reason}"),
            ApiError::Timeout => write!(f, "Execution timed out"),
            ApiError::Internal(msg) => write!(f, "Internal error: {msg}"),
        }
    }
}