Skip to content

Commit 6ba85e5

Browse files
authored
feat(wasm-runtime): add exec, time host function modules and type bridge (#18)
* feat(wasm-runtime-interface): add exec, time host modules and type bridge Extend the WASM host runtime with new host function modules for subprocess execution and timestamp access, a type bridge for WASM boundary serialization, and an end-to-end evaluation convenience method on ChallengeInstance. New modules: - exec.rs: `platform_exec` host function module with `exec_command` that runs sandboxed subprocess execution via std::process::Command. Enforces allow-lists, argument blocking, env var filtering, timeouts, output size limits, and per-instance execution count caps through a configurable ExecPolicy. Includes ExecState for tracking execution counts and ExecHostFunctions registrar. - time.rs: `platform_time` host function module with `get_timestamp` returning either a real system timestamp or a deterministic fixed value, controlled by TimePolicy (with Real and Deterministic modes). Includes TimeState and TimeHostFunctions registrar. - bridge.rs: Type bridge between SDK evaluation types and WASM boundary types. Defines EvalRequest/EvalResponse structs and provides conversion functions (request_to_input, input_to_bytes, bytes_to_output, output_to_response) that serialize via bincode for the WASM memory boundary. Uses EvaluationInput/ EvaluationOutput from platform-challenge-sdk-wasm. Runtime integration (runtime.rs): - RuntimeState now carries ExecState and TimeState alongside NetworkState - InstanceConfig extended with exec_policy and time_policy fields - ExecHostFunctions and TimeHostFunctions registered in the linker during instance creation, gated by policy configuration - Added BridgeError variant to WasmRuntimeError with From impl - ChallengeInstance gains evaluate_request() method for end-to-end evaluation: bridges EvalRequest to WASM input, allocates WASM memory, calls evaluate export, reads output, and bridges back to EvalResponse - Added exec_executions() and reset_exec_state() convenience methods Dependencies (Cargo.toml): - Added platform-challenge-sdk-wasm (path dependency) and serde_json * fix: add missing exec_policy and time_policy fields to InstanceConfig initializers * fix: add missing task_definition and environment_config fields to EvaluationInput * fix: add missing fields in bridge.rs test
1 parent af95d9f commit 6ba85e5

File tree

8 files changed

+1164
-3
lines changed

8 files changed

+1164
-3
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bins/validator-node/src/wasm_executor.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use std::sync::Arc;
66
use std::time::Instant;
77
use tracing::{debug, info};
88
use wasm_runtime_interface::{
9-
InstanceConfig, NetworkHostFunctions, NetworkPolicy, RuntimeConfig, WasmModule, WasmRuntime,
10-
WasmRuntimeError,
9+
ExecPolicy, InstanceConfig, NetworkHostFunctions, NetworkPolicy, RuntimeConfig, TimePolicy,
10+
WasmModule, WasmRuntime, WasmRuntimeError,
1111
};
1212

1313
pub struct WasmExecutorConfig {
@@ -83,6 +83,8 @@ impl WasmChallengeExecutor {
8383

8484
let instance_config = InstanceConfig {
8585
network_policy: network_policy.clone(),
86+
exec_policy: ExecPolicy::default(),
87+
time_policy: TimePolicy::default(),
8688
audit_logger: None,
8789
memory_export: "memory".to_string(),
8890
challenge_id: module_path.to_string(),
@@ -171,6 +173,8 @@ impl WasmChallengeExecutor {
171173

172174
let instance_config = InstanceConfig {
173175
network_policy: network_policy.clone(),
176+
exec_policy: ExecPolicy::default(),
177+
time_policy: TimePolicy::default(),
174178
audit_logger: None,
175179
memory_export: "memory".to_string(),
176180
challenge_id: module_path.to_string(),

crates/wasm-runtime-interface/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ edition.workspace = true
55

66
[dependencies]
77
serde = { workspace = true }
8+
serde_json = { workspace = true }
89
thiserror = { workspace = true }
910
tracing = { workspace = true }
1011
chrono = { workspace = true }
@@ -14,4 +15,5 @@ wasmtime = "41.0.3"
1415
bincode = { workspace = true }
1516
reqwest = { workspace = true, features = ["blocking", "rustls-tls"] }
1617
trust-dns-resolver = "0.23.2"
17-
sha2 = { workspace = true }
18+
sha2 = { workspace = true }
19+
platform-challenge-sdk-wasm = { path = "../challenge-sdk-wasm" }
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
use platform_challenge_sdk_wasm::{EvaluationInput, EvaluationOutput};
2+
use serde::{Deserialize, Serialize};
3+
4+
#[derive(Debug, Clone, Serialize, Deserialize)]
5+
pub struct EvalRequest {
6+
pub request_id: String,
7+
pub submission_id: String,
8+
pub participant_id: String,
9+
pub data: serde_json::Value,
10+
pub metadata: Option<serde_json::Value>,
11+
pub epoch: u64,
12+
pub deadline: Option<i64>,
13+
}
14+
15+
#[derive(Debug, Clone, Serialize, Deserialize)]
16+
pub struct EvalResponse {
17+
pub request_id: String,
18+
pub success: bool,
19+
pub error: Option<String>,
20+
pub score: f64,
21+
pub results: serde_json::Value,
22+
pub execution_time_ms: i64,
23+
pub cost: Option<f64>,
24+
}
25+
26+
impl EvalResponse {
27+
pub fn success(request_id: &str, score: f64, results: serde_json::Value) -> Self {
28+
Self {
29+
request_id: request_id.to_string(),
30+
success: true,
31+
error: None,
32+
score,
33+
results,
34+
execution_time_ms: 0,
35+
cost: None,
36+
}
37+
}
38+
39+
pub fn error(request_id: &str, error: impl Into<String>) -> Self {
40+
Self {
41+
request_id: request_id.to_string(),
42+
success: false,
43+
error: Some(error.into()),
44+
score: 0.0,
45+
results: serde_json::Value::Null,
46+
execution_time_ms: 0,
47+
cost: None,
48+
}
49+
}
50+
51+
pub fn with_time(mut self, ms: i64) -> Self {
52+
self.execution_time_ms = ms;
53+
self
54+
}
55+
56+
pub fn with_cost(mut self, cost: f64) -> Self {
57+
self.cost = Some(cost);
58+
self
59+
}
60+
}
61+
62+
pub fn request_to_input(
63+
req: &EvalRequest,
64+
challenge_id: &str,
65+
) -> Result<EvaluationInput, BridgeError> {
66+
let agent_data =
67+
serde_json::to_vec(&req.data).map_err(|e| BridgeError::Serialize(format!("data: {e}")))?;
68+
69+
let params = match &req.metadata {
70+
Some(meta) => serde_json::to_vec(meta)
71+
.map_err(|e| BridgeError::Serialize(format!("metadata: {e}")))?,
72+
None => Vec::new(),
73+
};
74+
75+
Ok(EvaluationInput {
76+
agent_data,
77+
challenge_id: challenge_id.to_string(),
78+
params,
79+
task_definition: None,
80+
environment_config: None,
81+
})
82+
}
83+
84+
pub fn input_to_bytes(input: &EvaluationInput) -> Result<Vec<u8>, BridgeError> {
85+
bincode::serialize(input).map_err(|e| BridgeError::Serialize(e.to_string()))
86+
}
87+
88+
pub fn bytes_to_output(bytes: &[u8]) -> Result<EvaluationOutput, BridgeError> {
89+
bincode::deserialize(bytes).map_err(|e| BridgeError::Deserialize(e.to_string()))
90+
}
91+
92+
pub fn output_to_response(
93+
output: &EvaluationOutput,
94+
request_id: &str,
95+
execution_time_ms: i64,
96+
) -> EvalResponse {
97+
if output.valid {
98+
let score = output.score as f64 / 100.0;
99+
let results = serde_json::json!({ "message": output.message });
100+
EvalResponse::success(request_id, score, results).with_time(execution_time_ms)
101+
} else {
102+
EvalResponse::error(request_id, &output.message).with_time(execution_time_ms)
103+
}
104+
}
105+
106+
#[derive(Debug, thiserror::Error)]
107+
pub enum BridgeError {
108+
#[error("serialization error: {0}")]
109+
Serialize(String),
110+
#[error("deserialization error: {0}")]
111+
Deserialize(String),
112+
}
113+
114+
#[cfg(test)]
115+
mod tests {
116+
use super::*;
117+
use serde_json::json;
118+
119+
#[test]
120+
fn test_request_to_input() {
121+
let req = EvalRequest {
122+
request_id: "req-1".into(),
123+
submission_id: "sub-1".into(),
124+
participant_id: "part-1".into(),
125+
data: json!({"code": "print('hello')"}),
126+
metadata: Some(json!({"lang": "python"})),
127+
epoch: 1,
128+
deadline: None,
129+
};
130+
131+
let input = request_to_input(&req, "test-challenge").unwrap();
132+
assert_eq!(input.challenge_id, "test-challenge");
133+
assert!(!input.agent_data.is_empty());
134+
assert!(!input.params.is_empty());
135+
136+
let data: serde_json::Value = serde_json::from_slice(&input.agent_data).unwrap();
137+
assert_eq!(data, json!({"code": "print('hello')"}));
138+
139+
let meta: serde_json::Value = serde_json::from_slice(&input.params).unwrap();
140+
assert_eq!(meta, json!({"lang": "python"}));
141+
}
142+
143+
#[test]
144+
fn test_request_to_input_no_metadata() {
145+
let req = EvalRequest {
146+
request_id: "req-1".into(),
147+
submission_id: "sub-1".into(),
148+
participant_id: "part-1".into(),
149+
data: json!("test"),
150+
metadata: None,
151+
epoch: 0,
152+
deadline: None,
153+
};
154+
155+
let input = request_to_input(&req, "ch").unwrap();
156+
assert!(input.params.is_empty());
157+
}
158+
159+
#[test]
160+
fn test_roundtrip_input_bytes() {
161+
let input = EvaluationInput {
162+
agent_data: vec![1, 2, 3],
163+
challenge_id: "test".into(),
164+
params: vec![4, 5, 6],
165+
task_definition: None,
166+
environment_config: None,
167+
};
168+
169+
let bytes = input_to_bytes(&input).unwrap();
170+
let recovered: EvaluationInput = bincode::deserialize(&bytes).unwrap();
171+
assert_eq!(recovered.agent_data, input.agent_data);
172+
assert_eq!(recovered.challenge_id, input.challenge_id);
173+
assert_eq!(recovered.params, input.params);
174+
}
175+
176+
#[test]
177+
fn test_bytes_to_output() {
178+
let output = EvaluationOutput::success(85, "great job");
179+
let bytes = bincode::serialize(&output).unwrap();
180+
let recovered = bytes_to_output(&bytes).unwrap();
181+
assert_eq!(recovered.score, 85);
182+
assert!(recovered.valid);
183+
assert_eq!(recovered.message, "great job");
184+
}
185+
186+
#[test]
187+
fn test_output_to_response_success() {
188+
let output = EvaluationOutput::success(100, "perfect");
189+
let resp = output_to_response(&output, "req-1", 42);
190+
assert!(resp.success);
191+
assert_eq!(resp.request_id, "req-1");
192+
assert!((resp.score - 1.0).abs() < f64::EPSILON);
193+
assert_eq!(resp.execution_time_ms, 42);
194+
assert!(resp.error.is_none());
195+
}
196+
197+
#[test]
198+
fn test_output_to_response_failure() {
199+
let output = EvaluationOutput::failure("bad input");
200+
let resp = output_to_response(&output, "req-2", 10);
201+
assert!(!resp.success);
202+
assert_eq!(resp.request_id, "req-2");
203+
assert!((resp.score - 0.0).abs() < f64::EPSILON);
204+
assert_eq!(resp.error.as_deref(), Some("bad input"));
205+
}
206+
}

0 commit comments

Comments
 (0)