From 694c0a3f4078f11b5d41e4ff284a311fceb1dddc Mon Sep 17 00:00:00 2001 From: echobt Date: Tue, 17 Feb 2026 08:38:15 +0000 Subject: [PATCH 1/2] feat(challenge-sdk-wasm): extend WASM guest SDK with term-challenge types and host functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add term-challenge evaluation model support to the WASM guest SDK, enabling challenge authors to define tasks, execute sandbox commands, and report per-task scoring breakdowns. New types (types.rs): TaskDefinition, SandboxExecRequest, SandboxExecResponse, TaskResult, and TermEvaluationParams provide no_std-compatible structures for task-based evaluation workflows. EvaluationOutput gains a `details` field for serialized per-task result breakdowns. A score_f64_scaled() helper converts f64 scores to i64 via fixed-point scaling (×10000). New host functions (host_functions.rs): Declares sandbox_exec, get_timestamp, and log_message under the platform_sandbox WASM import module, with safe Rust wrappers host_sandbox_exec, host_get_timestamp, and host_log. Extended Challenge trait (lib.rs): Adds optional tasks() and configure() methods with default implementations. The register_challenge! macro now exports get_tasks and configure as additional WASM ABI entry points. Allocator fix (alloc_impl.rs): Adds allocate(size, _align) as a no_mangle alias for alloc() to fix ABI mismatch where the validator calls with two parameters. ARENA_SIZE is now configurable via the large-arena feature flag (4 MiB when enabled, 1 MiB default). Cargo.toml: Adds default and large-arena feature flags. --- crates/challenge-sdk-wasm/Cargo.toml | 4 + crates/challenge-sdk-wasm/src/alloc_impl.rs | 11 ++- .../challenge-sdk-wasm/src/host_functions.rs | 32 +++++++ crates/challenge-sdk-wasm/src/lib.rs | 37 +++++++- crates/challenge-sdk-wasm/src/types.rs | 85 +++++++++++++++++++ 5 files changed, 167 insertions(+), 2 deletions(-) diff --git a/crates/challenge-sdk-wasm/Cargo.toml b/crates/challenge-sdk-wasm/Cargo.toml index 37991507..1f122200 100644 --- a/crates/challenge-sdk-wasm/Cargo.toml +++ b/crates/challenge-sdk-wasm/Cargo.toml @@ -10,3 +10,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } bincode = { version = "1.3", default-features = false } + +[features] +default = [] +large-arena = [] # 4 MiB arena instead of the default 1 MiB diff --git a/crates/challenge-sdk-wasm/src/alloc_impl.rs b/crates/challenge-sdk-wasm/src/alloc_impl.rs index 05763770..9ddb51ff 100644 --- a/crates/challenge-sdk-wasm/src/alloc_impl.rs +++ b/crates/challenge-sdk-wasm/src/alloc_impl.rs @@ -1,6 +1,10 @@ use core::cell::UnsafeCell; -const ARENA_SIZE: usize = 4 * 1024 * 1024; // 4 MiB +#[cfg(feature = "large-arena")] +const ARENA_SIZE: usize = 4 * 1024 * 1024; + +#[cfg(not(feature = "large-arena"))] +const ARENA_SIZE: usize = 1024 * 1024; struct BumpAllocator { arena: UnsafeCell<[u8; ARENA_SIZE]>, @@ -50,6 +54,11 @@ pub extern "C" fn alloc(size: i32) -> i32 { } } +#[no_mangle] +pub extern "C" fn allocate(size: i32, _align: i32) -> i32 { + alloc(size) +} + pub fn sdk_alloc(size: usize) -> *mut u8 { ALLOCATOR.alloc(size, 8) } diff --git a/crates/challenge-sdk-wasm/src/host_functions.rs b/crates/challenge-sdk-wasm/src/host_functions.rs index 03f67414..d36cbd89 100644 --- a/crates/challenge-sdk-wasm/src/host_functions.rs +++ b/crates/challenge-sdk-wasm/src/host_functions.rs @@ -183,3 +183,35 @@ pub fn host_random_seed(buf: &mut [u8]) -> Result<(), i32> { } Ok(()) } + +#[link(wasm_import_module = "platform_sandbox")] +extern "C" { + fn sandbox_exec(req_ptr: i32, req_len: i32, resp_ptr: i32, resp_len: i32) -> i32; + fn get_timestamp() -> i64; + fn log_message(level: i32, msg_ptr: i32, msg_len: i32); +} + +pub fn host_sandbox_exec(request: &[u8]) -> Result, i32> { + let mut response_buf = vec![0u8; 262144]; + let status = unsafe { + sandbox_exec( + request.as_ptr() as i32, + request.len() as i32, + response_buf.as_mut_ptr() as i32, + response_buf.len() as i32, + ) + }; + if status < 0 { + return Err(status); + } + response_buf.truncate(status as usize); + Ok(response_buf) +} + +pub fn host_get_timestamp() -> i64 { + unsafe { get_timestamp() } +} + +pub fn host_log(level: u8, msg: &str) { + unsafe { log_message(level as i32, msg.as_ptr() as i32, msg.len() as i32) } +} diff --git a/crates/challenge-sdk-wasm/src/lib.rs b/crates/challenge-sdk-wasm/src/lib.rs index 78f847fe..32f4ec8a 100644 --- a/crates/challenge-sdk-wasm/src/lib.rs +++ b/crates/challenge-sdk-wasm/src/lib.rs @@ -8,6 +8,10 @@ pub mod term_types; pub mod types; pub use term_types::*; +pub use types::{ + score_f64_scaled, SandboxExecRequest, SandboxExecResponse, TaskDefinition, TaskResult, + TermEvaluationParams, +}; pub use types::{EvaluationInput, EvaluationOutput}; pub trait Challenge { @@ -23,6 +27,12 @@ pub trait Challenge { fn setup_environment(&self, _config: &[u8]) -> bool { true } + + fn tasks(&self) -> alloc::vec::Vec { + alloc::vec::Vec::new() + } + + fn configure(&self, _config: &[u8]) {} } /// Pack a pointer and length into a single i64 value. @@ -36,7 +46,7 @@ pub fn pack_ptr_len(ptr: i32, len: i32) -> i64 { /// Register a [`Challenge`] implementation and export the required WASM ABI /// functions (`evaluate`, `validate`, `get_name`, `get_version`, -/// `generate_task`, `setup_environment`, and `alloc`). +/// `generate_task`, `setup_environment`, `get_tasks`, `configure`, and `alloc`). /// /// # Usage /// @@ -161,5 +171,30 @@ macro_rules! register_challenge { 0 } } + + #[no_mangle] + pub extern "C" fn get_tasks() -> i64 { + let output = <$ty as $crate::Challenge>::tasks(&_CHALLENGE); + if output.is_empty() { + return $crate::pack_ptr_len(0, 0); + } + let ptr = $crate::alloc_impl::sdk_alloc(output.len()); + if ptr.is_null() { + return $crate::pack_ptr_len(0, 0); + } + unsafe { + core::ptr::copy_nonoverlapping(output.as_ptr(), ptr, output.len()); + } + $crate::pack_ptr_len(ptr as i32, output.len() as i32) + } + + #[no_mangle] + pub extern "C" fn configure(config_ptr: i32, config_len: i32) -> i32 { + let slice = unsafe { + core::slice::from_raw_parts(config_ptr as *const u8, config_len as usize) + }; + <$ty as $crate::Challenge>::configure(&_CHALLENGE, slice); + 1 + } }; } diff --git a/crates/challenge-sdk-wasm/src/types.rs b/crates/challenge-sdk-wasm/src/types.rs index 7036a615..9d295438 100644 --- a/crates/challenge-sdk-wasm/src/types.rs +++ b/crates/challenge-sdk-wasm/src/types.rs @@ -17,6 +17,7 @@ pub struct EvaluationOutput { pub valid: bool, pub message: String, pub metrics: Option>, + pub details: Option>, } impl EvaluationOutput { @@ -26,6 +27,7 @@ impl EvaluationOutput { valid: true, message: String::from(message), metrics: None, + details: None, } } @@ -35,6 +37,7 @@ impl EvaluationOutput { valid: false, message: String::from(message), metrics: None, + details: None, } } @@ -42,4 +45,86 @@ impl EvaluationOutput { self.metrics = Some(metrics); self } + + pub fn with_details(mut self, details: Vec) -> Self { + self.details = Some(details); + self + } +} + +pub fn score_f64_scaled(value: f64) -> i64 { + (value * 10_000.0) as i64 +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TaskDefinition { + pub task_id: String, + pub description: String, + pub command: String, + pub expected_output: Option, + pub timeout_ms: u64, + pub scoring_criteria: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SandboxExecRequest { + pub command: String, + pub args: Vec, + pub env_vars: Vec<(String, String)>, + pub working_dir: Option, + pub stdin: Option>, + pub timeout_ms: u64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SandboxExecResponse { + pub exit_code: i32, + pub stdout: Vec, + pub stderr: Vec, + pub duration_ms: u64, +} + +impl SandboxExecResponse { + pub fn is_success(&self) -> bool { + self.exit_code == 0 + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TaskResult { + pub task_id: String, + pub passed: bool, + pub score: f64, + pub output: Option, + pub metrics: Option>, +} + +impl TaskResult { + pub fn success(task_id: &str, score: f64) -> Self { + Self { + task_id: String::from(task_id), + passed: true, + score, + output: None, + metrics: None, + } + } + + pub fn failure(task_id: &str, output: &str) -> Self { + Self { + task_id: String::from(task_id), + passed: false, + score: 0.0, + output: Some(String::from(output)), + metrics: None, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TermEvaluationParams { + pub challenge_id: String, + pub task_definitions: Vec, + pub timeout_ms: u64, + pub environment_config: Option>, } From 59b2e6b45becb154855539d691e672c73ef9b782 Mon Sep 17 00:00:00 2001 From: echobt Date: Tue, 17 Feb 2026 08:41:10 +0000 Subject: [PATCH 2/2] fix: add missing details field to EvaluationOutput initializer --- challenges/term-challenge/src/evaluation.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/challenges/term-challenge/src/evaluation.rs b/challenges/term-challenge/src/evaluation.rs index 818752a3..36e6a0c6 100644 --- a/challenges/term-challenge/src/evaluation.rs +++ b/challenges/term-challenge/src/evaluation.rs @@ -48,6 +48,7 @@ pub fn evaluate(input: EvaluationInput) -> EvaluationOutput { valid: true, message, metrics: None, + details: None, } }