From b3f1a8f2ca8a5a0638a7c6f9e3e75b23a91a5d12 Mon Sep 17 00:00:00 2001 From: echobt Date: Wed, 18 Feb 2026 18:31:46 +0000 Subject: [PATCH 1/5] refactor: remove term-challenge code from platform-v2 Migrate all term-challenge-specific code out of platform-v2 so it remains a generic framework. Term-challenge now lives in its own repository and imports platform-challenge-sdk-wasm as a git dependency. Removed challenge crates: - Delete challenges/term-challenge/ directory entirely - Delete challenges/term-challenge-wasm/ directory entirely - Remove both from workspace members in Cargo.toml - Update Cargo.lock to drop term-challenge packages Removed term-specific types from SDK: - Delete crates/challenge-sdk-wasm/src/term_types.rs (CommandRequest, CommandResult, FileReadRequest/Response, FileWriteRequest/Response, FileListRequest/Response, FileEntry, TermEvaluationMetrics, etc.) - Remove pub mod term_types and pub use term_types::* from SDK lib.rs - Remove TermEvaluationParams struct from SDK types.rs - Remove TermEvaluationParams from the re-export list in lib.rs Generalized term-challenge references across platform-v2: - wasm-runtime-interface: Rename SandboxPolicy::term_challenge() to default_challenge(), TerminalPolicy::term_challenge() to default_challenge(), update sandbox test accordingly, rename RuntimeConfig::term_challenge() to default_challenge() - bins/platform-cli: Remove hardcoded term-challenge default config, use empty HashMap for challenges - challenge-registry: Generalize comment from term-challenge to generic - secure-container-runtime: Replace term-challenge test strings with generic test-challenge names in policy.rs, ws_transport.rs, and integration_tests.rs - challenge-orchestrator (non-workspace): Replace term-challenge Docker volume names, container prefixes, and image references with generic names in docker.rs, evaluator.rs, and lib.rs Updated documentation: - AGENTS.md: Remove term-challenge from architecture table, mermaid diagram, and workspace crates list. Generalize code examples to use MyChallenge instead of TermChallenge. - challenges/README.md: Remove term-challenge-specific build section, keep generic challenge instructions - docs/validator_wasm_audit.md: Generalize term-challenge references Updated build/deploy configs: - docker/Dockerfile.challenge: Remove term-challenge-wasm build target, keep generic multi-stage structure - docker/docker-compose.yml: Replace term-challenge references with generic challenge placeholders - scripts/build-wasm.sh: Remove term-challenge-specific build lines BREAKING CHANGE: platform-challenge-sdk-wasm no longer exports term-challenge-specific types (TermEvaluationParams, CommandRequest, CommandResult, etc.). Consumers must define these types locally. --- AGENTS.md | 29 +- Cargo.lock | 18 -- Cargo.toml | 3 - bins/platform-cli/src/main.rs | 12 +- challenges/README.md | 17 -- challenges/term-challenge-wasm/Cargo.toml | 13 - challenges/term-challenge-wasm/src/lib.rs | 249 ------------------ challenges/term-challenge-wasm/src/scoring.rs | 111 -------- challenges/term-challenge-wasm/src/tasks.rs | 58 ---- challenges/term-challenge-wasm/src/types.rs | 119 --------- challenges/term-challenge/Cargo.toml | 13 - challenges/term-challenge/src/lib.rs | 249 ------------------ challenges/term-challenge/src/scoring.rs | 111 -------- challenges/term-challenge/src/tasks.rs | 58 ---- challenges/term-challenge/src/types.rs | 119 --------- crates/challenge-orchestrator/src/docker.rs | 30 +-- .../challenge-orchestrator/src/evaluator.rs | 6 +- crates/challenge-orchestrator/src/lib.rs | 10 +- crates/challenge-registry/src/registry.rs | 2 +- crates/challenge-sdk-wasm/src/lib.rs | 3 - crates/challenge-sdk-wasm/src/term_types.rs | 123 --------- crates/challenge-sdk-wasm/src/types.rs | 8 - crates/secure-container-runtime/src/policy.rs | 6 +- .../src/ws_transport.rs | 4 +- .../tests/integration_tests.rs | 8 +- crates/wasm-runtime-interface/src/lib.rs | 6 +- crates/wasm-runtime-interface/src/runtime.rs | 4 +- crates/wasm-runtime-interface/src/sandbox.rs | 4 +- crates/wasm-runtime-interface/src/terminal.rs | 4 +- docker/Dockerfile.challenge | 12 +- docker/docker-compose.yml | 71 +++-- docs/validator_wasm_audit.md | 4 +- scripts/build-wasm.sh | 5 - 33 files changed, 92 insertions(+), 1397 deletions(-) delete mode 100644 challenges/term-challenge-wasm/Cargo.toml delete mode 100644 challenges/term-challenge-wasm/src/lib.rs delete mode 100644 challenges/term-challenge-wasm/src/scoring.rs delete mode 100644 challenges/term-challenge-wasm/src/tasks.rs delete mode 100644 challenges/term-challenge-wasm/src/types.rs delete mode 100644 challenges/term-challenge/Cargo.toml delete mode 100644 challenges/term-challenge/src/lib.rs delete mode 100644 challenges/term-challenge/src/scoring.rs delete mode 100644 challenges/term-challenge/src/tasks.rs delete mode 100644 challenges/term-challenge/src/types.rs delete mode 100644 crates/challenge-sdk-wasm/src/term_types.rs diff --git a/AGENTS.md b/AGENTS.md index e8bf04ae..0c401dbf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,13 +13,7 @@ Each challenge defines: - Submission formats and requirements - Scoring algorithms -The `term-challenge` crate lives in-tree at `challenges/term-challenge/` and is compiled to WASM for production evaluation. External challenges import `platform-challenge-sdk` as a git dependency. - -| Challenge | Location | Description | -|-----------|----------|-------------| -| Terminal Bench | [`challenges/term-challenge/`](challenges/term-challenge/) | Terminal task benchmark (WASM evaluation module) | -| Terminal Bench v2 | [`challenges/term-challenge-wasm/`](challenges/term-challenge-wasm/) | Terminal benchmark with LLM judge support (WASM `cdylib`) | -| *(others)* | *(external repos or `challenges/` subdirectories)* | *(challenge-specific)* | +Challenge crates are maintained in their own repositories and import `platform-challenge-sdk-wasm` as a git dependency. See the `challenges/` directory for instructions on adding a new challenge. --- @@ -49,19 +43,19 @@ flowchart LR Develop your agent following the challenge-specific requirements. Challenge crates implement the `Challenge` trait from `platform-challenge-sdk-wasm`: ```rust -// Example: challenges/term-challenge/src/lib.rs +// Example: my-challenge/src/lib.rs use platform_challenge_sdk_wasm::{Challenge, EvaluationInput, EvaluationOutput}; -pub struct TermChallenge; +pub struct MyChallenge; -impl Challenge for TermChallenge { - fn name(&self) -> &'static str { "term-challenge" } +impl Challenge for MyChallenge { + fn name(&self) -> &'static str { "my-challenge" } fn version(&self) -> &'static str { "0.1.0" } fn evaluate(&self, input: EvaluationInput) -> EvaluationOutput { /* ... */ } fn validate(&self, input: EvaluationInput) -> bool { /* ... */ } } -platform_challenge_sdk_wasm::register_challenge!(TermChallenge, TermChallenge::new()); +platform_challenge_sdk_wasm::register_challenge!(MyChallenge, MyChallenge::new()); ``` **Check the challenge documentation** for the correct submission format and evaluation criteria. @@ -140,9 +134,8 @@ Each challenge defines its own scoring algorithm in its `evaluate()` method. Val Build and test challenge WASM modules locally: ```bash -# Build the WASM artifacts -cargo build --release --target wasm32-unknown-unknown -p term-challenge -cargo build --release --target wasm32-unknown-unknown -p term-challenge-wasm +# Build a challenge WASM artifact (example) +cargo build --release --target wasm32-unknown-unknown -p my-challenge # Run workspace tests cargo test @@ -163,8 +156,6 @@ flowchart TB Platform --> Validator[validator-node] Platform --> Runtime[wasm-runtime-interface] Platform --> P2P[p2p-consensus] - Platform --> TC[challenges/term-challenge] - Platform --> TCW[challenges/term-challenge-wasm] ``` **Workspace crates** (from `Cargo.toml`): @@ -184,8 +175,6 @@ flowchart TB - `bins/validator-node` — main validator binary - `bins/utils` — CLI utilities - `bins/mock-subtensor` — mock Bittensor node for testing -- `challenges/term-challenge` — Terminal Bench WASM challenge -- `challenges/term-challenge-wasm` — Terminal Bench v2 WASM challenge (LLM judge) - `tests` — integration tests **Non-workspace crate** (exists on disk but not in workspace members): @@ -198,7 +187,7 @@ flowchart TB ## Getting Started 1. **Choose a challenge** you want to participate in -2. **Read the challenge documentation** (e.g., `challenges/term-challenge/`) +2. **Read the challenge documentation** for your chosen challenge 3. **Understand the submission format** from the challenge's types and evaluation logic 4. **Submit** through the P2P network 5. **Monitor** your submission status and scores diff --git a/Cargo.lock b/Cargo.lock index 289dca5c..bbe33390 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7957,24 +7957,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "term-challenge" -version = "0.1.0" -dependencies = [ - "bincode", - "platform-challenge-sdk-wasm", - "serde", -] - -[[package]] -name = "term-challenge-wasm" -version = "0.1.0" -dependencies = [ - "bincode", - "platform-challenge-sdk-wasm", - "serde", -] - [[package]] name = "termcolor" version = "1.4.1" diff --git a/Cargo.toml b/Cargo.toml index df08de4b..c6805bef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,14 +14,11 @@ members = [ "crates/p2p-consensus", "crates/wasm-runtime-interface", "crates/challenge-sdk-wasm", - "challenges/term-challenge-wasm", "bins/validator-node", "bins/utils", "bins/mock-subtensor", "bins/platform-cli", "tests", - "challenges/term-challenge", - "challenges/term-challenge-wasm", ] # Note: Challenges are in separate repositories and import platform-challenge-sdk as a git dependency # Note: WASM runtime removed - updates via git, version checked at handshake diff --git a/bins/platform-cli/src/main.rs b/bins/platform-cli/src/main.rs index e871c7a8..960ca10e 100644 --- a/bins/platform-cli/src/main.rs +++ b/bins/platform-cli/src/main.rs @@ -49,22 +49,12 @@ fn default_true() -> bool { impl Default for PlatformConfig { fn default() -> Self { - let mut challenges = HashMap::new(); - challenges.insert( - "term-challenge".to_string(), - ChallengeConfig { - github_repo: "PlatformNetwork/term-challenge".to_string(), - binary_name: "term-cli".to_string(), - command_alias: "term".to_string(), - auto_update: true, - }, - ); Self { network: NetworkConfig { rpc_endpoint: "wss://chain.platform.network".to_string(), netuid: 100, }, - challenges, + challenges: HashMap::new(), } } } diff --git a/challenges/README.md b/challenges/README.md index 6065f462..388fb0d8 100644 --- a/challenges/README.md +++ b/challenges/README.md @@ -8,7 +8,6 @@ This directory contains challenge crates that integrate with the Platform valida challenges/ ├── README.md # This file ├── compiled/ # Built WASM artifacts (generated by build-wasm.sh) -├── term-challenge/ # Terminal benchmark challenge (WASM) └── [your-challenge]/ # Your custom challenge crate ``` @@ -53,19 +52,6 @@ sequenceDiagram - Must support state persistence for hot-reload. - Must produce deterministic results for consensus. -## Term Challenge (Terminal Bench) - -The `term-challenge-wasm` crate provides the Terminal Bench challenge as a WASM module. To build it: - -```bash -# Build term-challenge specifically -./scripts/build-wasm.sh term-challenge-wasm - -# The compiled WASM will be in challenges/compiled/term_challenge_wasm.wasm -``` - -See the [term-challenge repository](https://github.com/PlatformNetwork/term-challenge) for agent development, task definitions, and scoring details. - ## Build WASM Artifacts ```bash @@ -74,9 +60,6 @@ See the [term-challenge repository](https://github.com/PlatformNetwork/term-chal # Build all challenge crates (discovers crates under challenges/*/) ./scripts/build-wasm.sh - -# Example: build term-challenge -./scripts/build-wasm.sh term-challenge-wasm ``` The build script will: diff --git a/challenges/term-challenge-wasm/Cargo.toml b/challenges/term-challenge-wasm/Cargo.toml deleted file mode 100644 index bee688e2..00000000 --- a/challenges/term-challenge-wasm/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "term-challenge-wasm" -version.workspace = true -edition.workspace = true -description = "Terminal Benchmark Challenge ported to WASM (wasm32-unknown-unknown)" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -platform-challenge-sdk-wasm = { path = "../../crates/challenge-sdk-wasm" } -serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } -bincode = { version = "1.3", default-features = false } diff --git a/challenges/term-challenge-wasm/src/lib.rs b/challenges/term-challenge-wasm/src/lib.rs deleted file mode 100644 index 36d60798..00000000 --- a/challenges/term-challenge-wasm/src/lib.rs +++ /dev/null @@ -1,249 +0,0 @@ -#![no_std] - -extern crate alloc; - -mod scoring; -mod tasks; -mod types; - -use alloc::vec::Vec; -use bincode::Options; -use platform_challenge_sdk_wasm::host_functions::{ - host_consensus_get_epoch, host_http_post, host_storage_get, host_storage_set, -}; -use platform_challenge_sdk_wasm::{Challenge, EvaluationInput, EvaluationOutput}; - -use crate::scoring::{calculate_aggregate, format_summary, to_weight}; -use crate::types::{ - ChallengeParams, DatasetSelection, LlmJudgeRequest, LlmJudgeResponse, Submission, TaskResult, -}; - -use alloc::string::String; - -const MAX_SUBMISSION_SIZE: u64 = 64 * 1024 * 1024; -const MAX_PARAMS_SIZE: u64 = 4 * 1024 * 1024; -const MAX_LLM_RESPONSE_SIZE: u64 = 1024 * 1024; -const MAX_TASKS: usize = 256; -const EPOCH_RATE_LIMIT: u64 = 3; -const LLM_JUDGE_PASS_THRESHOLD: f64 = 0.5; -const SCORE_SCALE_FACTOR: f64 = 10_000.0; - -fn bincode_options_submission() -> impl Options { - bincode::DefaultOptions::new() - .with_limit(MAX_SUBMISSION_SIZE) - .with_fixint_encoding() - .allow_trailing_bytes() -} - -fn bincode_options_params() -> impl Options { - bincode::DefaultOptions::new() - .with_limit(MAX_PARAMS_SIZE) - .with_fixint_encoding() - .allow_trailing_bytes() -} - -fn bincode_options_llm() -> impl Options { - bincode::DefaultOptions::new() - .with_limit(MAX_LLM_RESPONSE_SIZE) - .with_fixint_encoding() - .allow_trailing_bytes() -} - -fn validate_task_result(result: &TaskResult) -> bool { - if result.task_id.is_empty() { - return false; - } - if !result.score.is_finite() || !(0.0..=1.0).contains(&result.score) { - return false; - } - true -} - -fn last_submission_key(miner_hotkey: &str) -> Vec { - let mut key = Vec::from(b"last_submission:" as &[u8]); - key.extend_from_slice(miner_hotkey.as_bytes()); - key -} - -fn get_last_submission_epoch(miner_hotkey: &str) -> Option { - let key = last_submission_key(miner_hotkey); - let data = host_storage_get(&key).ok()?; - if data.len() < 8 { - return None; - } - let mut buf = [0u8; 8]; - buf.copy_from_slice(&data[..8]); - Some(u64::from_le_bytes(buf)) -} - -fn set_last_submission_epoch(miner_hotkey: &str, epoch: u64) { - let key = last_submission_key(miner_hotkey); - let _ = host_storage_set(&key, &epoch.to_le_bytes()); -} - -pub struct TermChallengeWasm; - -impl Default for TermChallengeWasm { - fn default() -> Self { - Self - } -} - -impl TermChallengeWasm { - pub const fn new() -> Self { - Self - } - - fn try_llm_judge(url: &str, result: &TaskResult, instruction: &str) -> Option { - let request = LlmJudgeRequest { - task_id: result.task_id.clone(), - instruction: String::from(instruction), - agent_output: result.agent_output.clone(), - test_output: result.test_output.clone(), - }; - let url_bytes = url.as_bytes(); - let body = match bincode_options_llm().serialize(&request) { - Ok(b) => b, - Err(_) => return None, - }; - let response_bytes = match host_http_post(url_bytes, &body) { - Ok(b) => b, - Err(_) => return None, - }; - let judge_resp: LlmJudgeResponse = match bincode_options_llm().deserialize(&response_bytes) - { - Ok(r) => r, - Err(_) => return None, - }; - if !judge_resp.score.is_finite() { - return None; - } - Some(judge_resp.score.clamp(0.0, 1.0)) - } -} - -impl Challenge for TermChallengeWasm { - fn name(&self) -> &'static str { - "term-challenge" - } - - fn version(&self) -> &'static str { - "4.0.0" - } - - fn evaluate(&self, input: EvaluationInput) -> EvaluationOutput { - let submission: Submission = - match bincode_options_submission().deserialize(&input.agent_data) { - Ok(s) => s, - Err(_) => return EvaluationOutput::failure("failed to deserialize submission"), - }; - let params: ChallengeParams = match bincode_options_params().deserialize(&input.params) { - Ok(p) => p, - Err(_) => return EvaluationOutput::failure("failed to deserialize challenge params"), - }; - if submission.task_results.is_empty() { - return EvaluationOutput::failure("submission contains no task results"); - } - if submission.task_results.len() > MAX_TASKS { - return EvaluationOutput::failure("submission exceeds maximum task count"); - } - if submission.task_results.len() != params.tasks.len() { - return EvaluationOutput::failure("task result count does not match task definitions"); - } - for result in &submission.task_results { - if !validate_task_result(result) { - return EvaluationOutput::failure( - "invalid task result: bad score or empty task_id", - ); - } - } - let mut results: Vec = submission.task_results; - if let Some(ref url) = params.llm_judge_url { - for (result, task) in results.iter_mut().zip(params.tasks.iter()) { - if !result.passed { - continue; - } - if let Some(llm_score) = Self::try_llm_judge(url, result, &task.name) { - result.score = llm_score; - if llm_score < LLM_JUDGE_PASS_THRESHOLD { - result.passed = false; - } - } - } - } - let aggregate = calculate_aggregate(¶ms.tasks, &results); - let weight = to_weight(&aggregate); - let score = (weight * SCORE_SCALE_FACTOR) as i64; - let message = format_summary(&aggregate); - set_last_submission_epoch(&submission.miner_hotkey, submission.epoch); - EvaluationOutput::success(score, &message) - } - - fn validate(&self, input: EvaluationInput) -> bool { - let submission: Submission = - match bincode_options_submission().deserialize(&input.agent_data) { - Ok(s) => s, - Err(_) => return false, - }; - let params: ChallengeParams = match bincode_options_params().deserialize(&input.params) { - Ok(p) => p, - Err(_) => return false, - }; - if submission.agent_hash.is_empty() || submission.miner_hotkey.is_empty() { - return false; - } - if submission.signature.is_empty() { - return false; - } - if submission.package_zip.is_empty() { - return false; - } - if submission.basilica_instance.is_empty() - || submission.executor_url.is_empty() - || submission.executor_token.is_empty() - { - return false; - } - let current_epoch = host_consensus_get_epoch(); - if current_epoch >= 0 { - if let Some(last_epoch) = get_last_submission_epoch(&submission.miner_hotkey) { - let current = current_epoch as u64; - if current < last_epoch.saturating_add(EPOCH_RATE_LIMIT) { - return false; - } - } - } - if submission.task_results.is_empty() { - return false; - } - if submission.task_results.len() > MAX_TASKS { - return false; - } - if submission.task_results.len() != params.tasks.len() { - return false; - } - for result in &submission.task_results { - if !validate_task_result(result) { - return false; - } - } - true - } - - fn tasks(&self) -> Vec { - match tasks::get_active_dataset() { - Some(task_defs) => bincode_options_params() - .serialize(&task_defs) - .unwrap_or_default(), - None => Vec::new(), - } - } - - fn configure(&self, config: &[u8]) { - if let Ok(selection) = bincode_options_params().deserialize::(config) { - let _ = tasks::store_dataset(&selection); - } - } -} - -platform_challenge_sdk_wasm::register_challenge!(TermChallengeWasm, TermChallengeWasm::new()); diff --git a/challenges/term-challenge-wasm/src/scoring.rs b/challenges/term-challenge-wasm/src/scoring.rs deleted file mode 100644 index eb9047ab..00000000 --- a/challenges/term-challenge-wasm/src/scoring.rs +++ /dev/null @@ -1,111 +0,0 @@ -use alloc::string::String; -use core::fmt::Write as _; - -use crate::types::{Difficulty, DifficultyStats, TaskDefinition, TaskResult}; - -pub struct AggregateScore { - pub tasks_passed: u32, - pub tasks_failed: u32, - pub pass_rate: f64, - pub total_execution_time_ms: u64, - pub easy_stats: DifficultyStats, - pub medium_stats: DifficultyStats, - pub hard_stats: DifficultyStats, -} - -impl AggregateScore { - pub fn total_tasks(&self) -> u32 { - self.tasks_passed.saturating_add(self.tasks_failed) - } -} - -pub fn calculate_aggregate(tasks: &[TaskDefinition], results: &[TaskResult]) -> AggregateScore { - let mut passed: u32 = 0; - let mut failed: u32 = 0; - let mut total_execution_time_ms: u64 = 0; - let mut easy = DifficultyStats { - total: 0, - passed: 0, - }; - let mut medium = DifficultyStats { - total: 0, - passed: 0, - }; - let mut hard = DifficultyStats { - total: 0, - passed: 0, - }; - - for (task, result) in tasks.iter().zip(results.iter()) { - if result.passed { - passed += 1; - } else { - failed += 1; - } - total_execution_time_ms = total_execution_time_ms.saturating_add(result.execution_time_ms); - let stats = match task.difficulty { - Difficulty::Easy => &mut easy, - Difficulty::Medium => &mut medium, - Difficulty::Hard => &mut hard, - }; - stats.total += 1; - if result.passed { - stats.passed += 1; - } - } - - let total = passed + failed; - let pass_rate = if total > 0 { - passed as f64 / total as f64 - } else { - 0.0 - }; - - AggregateScore { - tasks_passed: passed, - tasks_failed: failed, - pass_rate, - total_execution_time_ms, - easy_stats: easy, - medium_stats: medium, - hard_stats: hard, - } -} - -pub fn to_weight(score: &AggregateScore) -> f64 { - score.pass_rate.clamp(0.0, 1.0) -} - -pub fn format_summary(score: &AggregateScore) -> String { - let mut msg = String::new(); - let _ = write!( - msg, - "passed={}/{} rate={:.2}%", - score.tasks_passed, - score.total_tasks(), - score.pass_rate * 100.0, - ); - if score.easy_stats.total > 0 { - let _ = write!( - msg, - " easy={}/{}", - score.easy_stats.passed, score.easy_stats.total - ); - } - if score.medium_stats.total > 0 { - let _ = write!( - msg, - " med={}/{}", - score.medium_stats.passed, score.medium_stats.total - ); - } - if score.hard_stats.total > 0 { - let _ = write!( - msg, - " hard={}/{}", - score.hard_stats.passed, score.hard_stats.total - ); - } - let _ = write!(msg, " time={}ms", score.total_execution_time_ms); - msg -} diff --git a/challenges/term-challenge-wasm/src/tasks.rs b/challenges/term-challenge-wasm/src/tasks.rs deleted file mode 100644 index 192fbb7f..00000000 --- a/challenges/term-challenge-wasm/src/tasks.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::types::{DatasetSelection, TaskDefinition}; -use alloc::vec::Vec; -use bincode::Options; -use platform_challenge_sdk_wasm::host_functions::{host_storage_get, host_storage_set}; - -const ACTIVE_DATASET_KEY: &[u8] = b"active_dataset"; -const DATASET_HISTORY_KEY: &[u8] = b"dataset_history"; -const MAX_DATASET_SIZE: u64 = 8 * 1024 * 1024; -const MAX_DATASET_HISTORY: usize = 100; - -fn bincode_options_dataset() -> impl Options { - bincode::DefaultOptions::new() - .with_limit(MAX_DATASET_SIZE) - .with_fixint_encoding() - .allow_trailing_bytes() -} - -pub fn get_active_dataset() -> Option> { - let data = host_storage_get(ACTIVE_DATASET_KEY).ok()?; - if data.is_empty() { - return None; - } - bincode_options_dataset().deserialize(&data).ok() -} - -pub fn store_dataset(selection: &DatasetSelection) -> bool { - let data = match bincode_options_dataset().serialize(selection) { - Ok(d) => d, - Err(_) => return false, - }; - if host_storage_set(ACTIVE_DATASET_KEY, &data).is_err() { - return false; - } - let _ = append_dataset_history(selection); - true -} - -fn append_dataset_history(selection: &DatasetSelection) -> bool { - let mut history: Vec = host_storage_get(DATASET_HISTORY_KEY) - .ok() - .and_then(|d| { - if d.is_empty() { - None - } else { - bincode_options_dataset().deserialize(&d).ok() - } - }) - .unwrap_or_default(); - history.push(selection.clone()); - if history.len() > MAX_DATASET_HISTORY { - history.drain(0..history.len() - MAX_DATASET_HISTORY); - } - let data = match bincode_options_dataset().serialize(&history) { - Ok(d) => d, - Err(_) => return false, - }; - host_storage_set(DATASET_HISTORY_KEY, &data).is_ok() -} diff --git a/challenges/term-challenge-wasm/src/types.rs b/challenges/term-challenge-wasm/src/types.rs deleted file mode 100644 index 6c218aec..00000000 --- a/challenges/term-challenge-wasm/src/types.rs +++ /dev/null @@ -1,119 +0,0 @@ -use alloc::string::String; -use alloc::vec::Vec; -use core::fmt; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum Difficulty { - Easy, - Medium, - Hard, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct TaskDefinition { - pub id: String, - pub name: String, - pub repo: String, - pub base_commit: String, - pub difficulty: Difficulty, - pub timeout_secs: u64, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct TaskResult { - pub task_id: String, - pub passed: bool, - pub score: f64, - pub execution_time_ms: u64, - pub test_output: String, - pub agent_output: String, - pub error: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ChallengeParams { - pub tasks: Vec, - pub llm_judge_url: Option, - pub decay_params: Option, - pub active_dataset: Option>, -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct Submission { - pub agent_hash: String, - pub miner_hotkey: String, - pub signature: Vec, - pub epoch: u64, - pub package_zip: Vec, - pub basilica_instance: String, - pub executor_url: String, - pub executor_token: String, - pub task_results: Vec, -} - -impl fmt::Debug for Submission { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Submission") - .field("agent_hash", &self.agent_hash) - .field("miner_hotkey", &self.miner_hotkey) - .field( - "signature", - &format_args!("[{} bytes]", self.signature.len()), - ) - .field("epoch", &self.epoch) - .field( - "package_zip", - &format_args!("[{} bytes]", self.package_zip.len()), - ) - .field("basilica_instance", &self.basilica_instance) - .field("executor_url", &self.executor_url) - .field("executor_token", &"[REDACTED]") - .field("task_results", &self.task_results) - .finish() - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct DifficultyStats { - pub total: u32, - pub passed: u32, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct LlmJudgeRequest { - pub task_id: String, - pub instruction: String, - pub agent_output: String, - pub test_output: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct LlmJudgeResponse { - pub score: f64, - pub reasoning: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct DecayParams { - pub grace_period_hours: u64, - pub half_life_hours: u64, - pub min_multiplier: f64, -} - -impl Default for DecayParams { - fn default() -> Self { - Self { - grace_period_hours: 72, - half_life_hours: 24, - min_multiplier: 0.0, - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct DatasetSelection { - pub tasks: Vec, - pub selected_at_epoch: u64, - pub dataset_hash: String, -} diff --git a/challenges/term-challenge/Cargo.toml b/challenges/term-challenge/Cargo.toml deleted file mode 100644 index ba9e92a9..00000000 --- a/challenges/term-challenge/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "term-challenge" -version.workspace = true -edition.workspace = true -description = "Terminal benchmark challenge – WASM evaluation module" - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -platform-challenge-sdk-wasm = { path = "../../crates/challenge-sdk-wasm" } -serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } -bincode = { version = "1.3", default-features = false } diff --git a/challenges/term-challenge/src/lib.rs b/challenges/term-challenge/src/lib.rs deleted file mode 100644 index 36d60798..00000000 --- a/challenges/term-challenge/src/lib.rs +++ /dev/null @@ -1,249 +0,0 @@ -#![no_std] - -extern crate alloc; - -mod scoring; -mod tasks; -mod types; - -use alloc::vec::Vec; -use bincode::Options; -use platform_challenge_sdk_wasm::host_functions::{ - host_consensus_get_epoch, host_http_post, host_storage_get, host_storage_set, -}; -use platform_challenge_sdk_wasm::{Challenge, EvaluationInput, EvaluationOutput}; - -use crate::scoring::{calculate_aggregate, format_summary, to_weight}; -use crate::types::{ - ChallengeParams, DatasetSelection, LlmJudgeRequest, LlmJudgeResponse, Submission, TaskResult, -}; - -use alloc::string::String; - -const MAX_SUBMISSION_SIZE: u64 = 64 * 1024 * 1024; -const MAX_PARAMS_SIZE: u64 = 4 * 1024 * 1024; -const MAX_LLM_RESPONSE_SIZE: u64 = 1024 * 1024; -const MAX_TASKS: usize = 256; -const EPOCH_RATE_LIMIT: u64 = 3; -const LLM_JUDGE_PASS_THRESHOLD: f64 = 0.5; -const SCORE_SCALE_FACTOR: f64 = 10_000.0; - -fn bincode_options_submission() -> impl Options { - bincode::DefaultOptions::new() - .with_limit(MAX_SUBMISSION_SIZE) - .with_fixint_encoding() - .allow_trailing_bytes() -} - -fn bincode_options_params() -> impl Options { - bincode::DefaultOptions::new() - .with_limit(MAX_PARAMS_SIZE) - .with_fixint_encoding() - .allow_trailing_bytes() -} - -fn bincode_options_llm() -> impl Options { - bincode::DefaultOptions::new() - .with_limit(MAX_LLM_RESPONSE_SIZE) - .with_fixint_encoding() - .allow_trailing_bytes() -} - -fn validate_task_result(result: &TaskResult) -> bool { - if result.task_id.is_empty() { - return false; - } - if !result.score.is_finite() || !(0.0..=1.0).contains(&result.score) { - return false; - } - true -} - -fn last_submission_key(miner_hotkey: &str) -> Vec { - let mut key = Vec::from(b"last_submission:" as &[u8]); - key.extend_from_slice(miner_hotkey.as_bytes()); - key -} - -fn get_last_submission_epoch(miner_hotkey: &str) -> Option { - let key = last_submission_key(miner_hotkey); - let data = host_storage_get(&key).ok()?; - if data.len() < 8 { - return None; - } - let mut buf = [0u8; 8]; - buf.copy_from_slice(&data[..8]); - Some(u64::from_le_bytes(buf)) -} - -fn set_last_submission_epoch(miner_hotkey: &str, epoch: u64) { - let key = last_submission_key(miner_hotkey); - let _ = host_storage_set(&key, &epoch.to_le_bytes()); -} - -pub struct TermChallengeWasm; - -impl Default for TermChallengeWasm { - fn default() -> Self { - Self - } -} - -impl TermChallengeWasm { - pub const fn new() -> Self { - Self - } - - fn try_llm_judge(url: &str, result: &TaskResult, instruction: &str) -> Option { - let request = LlmJudgeRequest { - task_id: result.task_id.clone(), - instruction: String::from(instruction), - agent_output: result.agent_output.clone(), - test_output: result.test_output.clone(), - }; - let url_bytes = url.as_bytes(); - let body = match bincode_options_llm().serialize(&request) { - Ok(b) => b, - Err(_) => return None, - }; - let response_bytes = match host_http_post(url_bytes, &body) { - Ok(b) => b, - Err(_) => return None, - }; - let judge_resp: LlmJudgeResponse = match bincode_options_llm().deserialize(&response_bytes) - { - Ok(r) => r, - Err(_) => return None, - }; - if !judge_resp.score.is_finite() { - return None; - } - Some(judge_resp.score.clamp(0.0, 1.0)) - } -} - -impl Challenge for TermChallengeWasm { - fn name(&self) -> &'static str { - "term-challenge" - } - - fn version(&self) -> &'static str { - "4.0.0" - } - - fn evaluate(&self, input: EvaluationInput) -> EvaluationOutput { - let submission: Submission = - match bincode_options_submission().deserialize(&input.agent_data) { - Ok(s) => s, - Err(_) => return EvaluationOutput::failure("failed to deserialize submission"), - }; - let params: ChallengeParams = match bincode_options_params().deserialize(&input.params) { - Ok(p) => p, - Err(_) => return EvaluationOutput::failure("failed to deserialize challenge params"), - }; - if submission.task_results.is_empty() { - return EvaluationOutput::failure("submission contains no task results"); - } - if submission.task_results.len() > MAX_TASKS { - return EvaluationOutput::failure("submission exceeds maximum task count"); - } - if submission.task_results.len() != params.tasks.len() { - return EvaluationOutput::failure("task result count does not match task definitions"); - } - for result in &submission.task_results { - if !validate_task_result(result) { - return EvaluationOutput::failure( - "invalid task result: bad score or empty task_id", - ); - } - } - let mut results: Vec = submission.task_results; - if let Some(ref url) = params.llm_judge_url { - for (result, task) in results.iter_mut().zip(params.tasks.iter()) { - if !result.passed { - continue; - } - if let Some(llm_score) = Self::try_llm_judge(url, result, &task.name) { - result.score = llm_score; - if llm_score < LLM_JUDGE_PASS_THRESHOLD { - result.passed = false; - } - } - } - } - let aggregate = calculate_aggregate(¶ms.tasks, &results); - let weight = to_weight(&aggregate); - let score = (weight * SCORE_SCALE_FACTOR) as i64; - let message = format_summary(&aggregate); - set_last_submission_epoch(&submission.miner_hotkey, submission.epoch); - EvaluationOutput::success(score, &message) - } - - fn validate(&self, input: EvaluationInput) -> bool { - let submission: Submission = - match bincode_options_submission().deserialize(&input.agent_data) { - Ok(s) => s, - Err(_) => return false, - }; - let params: ChallengeParams = match bincode_options_params().deserialize(&input.params) { - Ok(p) => p, - Err(_) => return false, - }; - if submission.agent_hash.is_empty() || submission.miner_hotkey.is_empty() { - return false; - } - if submission.signature.is_empty() { - return false; - } - if submission.package_zip.is_empty() { - return false; - } - if submission.basilica_instance.is_empty() - || submission.executor_url.is_empty() - || submission.executor_token.is_empty() - { - return false; - } - let current_epoch = host_consensus_get_epoch(); - if current_epoch >= 0 { - if let Some(last_epoch) = get_last_submission_epoch(&submission.miner_hotkey) { - let current = current_epoch as u64; - if current < last_epoch.saturating_add(EPOCH_RATE_LIMIT) { - return false; - } - } - } - if submission.task_results.is_empty() { - return false; - } - if submission.task_results.len() > MAX_TASKS { - return false; - } - if submission.task_results.len() != params.tasks.len() { - return false; - } - for result in &submission.task_results { - if !validate_task_result(result) { - return false; - } - } - true - } - - fn tasks(&self) -> Vec { - match tasks::get_active_dataset() { - Some(task_defs) => bincode_options_params() - .serialize(&task_defs) - .unwrap_or_default(), - None => Vec::new(), - } - } - - fn configure(&self, config: &[u8]) { - if let Ok(selection) = bincode_options_params().deserialize::(config) { - let _ = tasks::store_dataset(&selection); - } - } -} - -platform_challenge_sdk_wasm::register_challenge!(TermChallengeWasm, TermChallengeWasm::new()); diff --git a/challenges/term-challenge/src/scoring.rs b/challenges/term-challenge/src/scoring.rs deleted file mode 100644 index eb9047ab..00000000 --- a/challenges/term-challenge/src/scoring.rs +++ /dev/null @@ -1,111 +0,0 @@ -use alloc::string::String; -use core::fmt::Write as _; - -use crate::types::{Difficulty, DifficultyStats, TaskDefinition, TaskResult}; - -pub struct AggregateScore { - pub tasks_passed: u32, - pub tasks_failed: u32, - pub pass_rate: f64, - pub total_execution_time_ms: u64, - pub easy_stats: DifficultyStats, - pub medium_stats: DifficultyStats, - pub hard_stats: DifficultyStats, -} - -impl AggregateScore { - pub fn total_tasks(&self) -> u32 { - self.tasks_passed.saturating_add(self.tasks_failed) - } -} - -pub fn calculate_aggregate(tasks: &[TaskDefinition], results: &[TaskResult]) -> AggregateScore { - let mut passed: u32 = 0; - let mut failed: u32 = 0; - let mut total_execution_time_ms: u64 = 0; - let mut easy = DifficultyStats { - total: 0, - passed: 0, - }; - let mut medium = DifficultyStats { - total: 0, - passed: 0, - }; - let mut hard = DifficultyStats { - total: 0, - passed: 0, - }; - - for (task, result) in tasks.iter().zip(results.iter()) { - if result.passed { - passed += 1; - } else { - failed += 1; - } - total_execution_time_ms = total_execution_time_ms.saturating_add(result.execution_time_ms); - let stats = match task.difficulty { - Difficulty::Easy => &mut easy, - Difficulty::Medium => &mut medium, - Difficulty::Hard => &mut hard, - }; - stats.total += 1; - if result.passed { - stats.passed += 1; - } - } - - let total = passed + failed; - let pass_rate = if total > 0 { - passed as f64 / total as f64 - } else { - 0.0 - }; - - AggregateScore { - tasks_passed: passed, - tasks_failed: failed, - pass_rate, - total_execution_time_ms, - easy_stats: easy, - medium_stats: medium, - hard_stats: hard, - } -} - -pub fn to_weight(score: &AggregateScore) -> f64 { - score.pass_rate.clamp(0.0, 1.0) -} - -pub fn format_summary(score: &AggregateScore) -> String { - let mut msg = String::new(); - let _ = write!( - msg, - "passed={}/{} rate={:.2}%", - score.tasks_passed, - score.total_tasks(), - score.pass_rate * 100.0, - ); - if score.easy_stats.total > 0 { - let _ = write!( - msg, - " easy={}/{}", - score.easy_stats.passed, score.easy_stats.total - ); - } - if score.medium_stats.total > 0 { - let _ = write!( - msg, - " med={}/{}", - score.medium_stats.passed, score.medium_stats.total - ); - } - if score.hard_stats.total > 0 { - let _ = write!( - msg, - " hard={}/{}", - score.hard_stats.passed, score.hard_stats.total - ); - } - let _ = write!(msg, " time={}ms", score.total_execution_time_ms); - msg -} diff --git a/challenges/term-challenge/src/tasks.rs b/challenges/term-challenge/src/tasks.rs deleted file mode 100644 index 192fbb7f..00000000 --- a/challenges/term-challenge/src/tasks.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::types::{DatasetSelection, TaskDefinition}; -use alloc::vec::Vec; -use bincode::Options; -use platform_challenge_sdk_wasm::host_functions::{host_storage_get, host_storage_set}; - -const ACTIVE_DATASET_KEY: &[u8] = b"active_dataset"; -const DATASET_HISTORY_KEY: &[u8] = b"dataset_history"; -const MAX_DATASET_SIZE: u64 = 8 * 1024 * 1024; -const MAX_DATASET_HISTORY: usize = 100; - -fn bincode_options_dataset() -> impl Options { - bincode::DefaultOptions::new() - .with_limit(MAX_DATASET_SIZE) - .with_fixint_encoding() - .allow_trailing_bytes() -} - -pub fn get_active_dataset() -> Option> { - let data = host_storage_get(ACTIVE_DATASET_KEY).ok()?; - if data.is_empty() { - return None; - } - bincode_options_dataset().deserialize(&data).ok() -} - -pub fn store_dataset(selection: &DatasetSelection) -> bool { - let data = match bincode_options_dataset().serialize(selection) { - Ok(d) => d, - Err(_) => return false, - }; - if host_storage_set(ACTIVE_DATASET_KEY, &data).is_err() { - return false; - } - let _ = append_dataset_history(selection); - true -} - -fn append_dataset_history(selection: &DatasetSelection) -> bool { - let mut history: Vec = host_storage_get(DATASET_HISTORY_KEY) - .ok() - .and_then(|d| { - if d.is_empty() { - None - } else { - bincode_options_dataset().deserialize(&d).ok() - } - }) - .unwrap_or_default(); - history.push(selection.clone()); - if history.len() > MAX_DATASET_HISTORY { - history.drain(0..history.len() - MAX_DATASET_HISTORY); - } - let data = match bincode_options_dataset().serialize(&history) { - Ok(d) => d, - Err(_) => return false, - }; - host_storage_set(DATASET_HISTORY_KEY, &data).is_ok() -} diff --git a/challenges/term-challenge/src/types.rs b/challenges/term-challenge/src/types.rs deleted file mode 100644 index 6c218aec..00000000 --- a/challenges/term-challenge/src/types.rs +++ /dev/null @@ -1,119 +0,0 @@ -use alloc::string::String; -use alloc::vec::Vec; -use core::fmt; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum Difficulty { - Easy, - Medium, - Hard, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct TaskDefinition { - pub id: String, - pub name: String, - pub repo: String, - pub base_commit: String, - pub difficulty: Difficulty, - pub timeout_secs: u64, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct TaskResult { - pub task_id: String, - pub passed: bool, - pub score: f64, - pub execution_time_ms: u64, - pub test_output: String, - pub agent_output: String, - pub error: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ChallengeParams { - pub tasks: Vec, - pub llm_judge_url: Option, - pub decay_params: Option, - pub active_dataset: Option>, -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct Submission { - pub agent_hash: String, - pub miner_hotkey: String, - pub signature: Vec, - pub epoch: u64, - pub package_zip: Vec, - pub basilica_instance: String, - pub executor_url: String, - pub executor_token: String, - pub task_results: Vec, -} - -impl fmt::Debug for Submission { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Submission") - .field("agent_hash", &self.agent_hash) - .field("miner_hotkey", &self.miner_hotkey) - .field( - "signature", - &format_args!("[{} bytes]", self.signature.len()), - ) - .field("epoch", &self.epoch) - .field( - "package_zip", - &format_args!("[{} bytes]", self.package_zip.len()), - ) - .field("basilica_instance", &self.basilica_instance) - .field("executor_url", &self.executor_url) - .field("executor_token", &"[REDACTED]") - .field("task_results", &self.task_results) - .finish() - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct DifficultyStats { - pub total: u32, - pub passed: u32, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct LlmJudgeRequest { - pub task_id: String, - pub instruction: String, - pub agent_output: String, - pub test_output: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct LlmJudgeResponse { - pub score: f64, - pub reasoning: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct DecayParams { - pub grace_period_hours: u64, - pub half_life_hours: u64, - pub min_multiplier: f64, -} - -impl Default for DecayParams { - fn default() -> Self { - Self { - grace_period_hours: 72, - half_life_hours: 24, - min_multiplier: 0.0, - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct DatasetSelection { - pub tasks: Vec, - pub selected_at_epoch: u64, - pub dataset_hash: String, -} diff --git a/crates/challenge-orchestrator/src/docker.rs b/crates/challenge-orchestrator/src/docker.rs index 96216bd1..2f0f6a8d 100644 --- a/crates/challenge-orchestrator/src/docker.rs +++ b/crates/challenge-orchestrator/src/docker.rs @@ -744,9 +744,9 @@ impl DockerClient { // Create named volumes for Docker-in-Docker task sharing // These volumes are shared between challenge containers and agent containers - let tasks_volume = "term-challenge-tasks"; - let dind_cache_volume = "term-challenge-cache"; - let evals_volume = "term-challenge-evals"; + let tasks_volume = "challenge-tasks"; + let dind_cache_volume = "challenge-cache"; + let evals_volume = "challenge-evals"; for vol_name in [tasks_volume, dind_cache_volume, evals_volume] { let vol_opts = CreateVolumeOptions { @@ -778,13 +778,13 @@ impl DockerClient { tasks_volume, tasks_volume ), // Cache volume - for downloaded datasets - format!("{}:/root/.cache/term-challenge:rw", dind_cache_volume), + format!("{}:/root/.cache/challenge:rw", dind_cache_volume), format!( "{}:/var/lib/docker/volumes/{}/_data:rw", dind_cache_volume, dind_cache_volume ), // Evals volume - for evaluation logs - format!("{}:/tmp/term-challenge-evals:rw", evals_volume), + format!("{}:/tmp/challenge-evals:rw", evals_volume), format!( "{}:/var/lib/docker/volumes/{}/_data:rw", evals_volume, evals_volume @@ -821,21 +821,21 @@ impl DockerClient { let rust_log = if std::env::var("VERBOSE").is_ok() { "debug,hyper=info,h2=info,tower=info,tokio_postgres=debug".to_string() } else { - "info,term_challenge=debug".to_string() + "info,challenge=debug".to_string() }; env.push(format!("RUST_LOG={}", rust_log)); // Force challenge server to listen on port 8080 (orchestrator expects this) env.push("PORT=8080".to_string()); // For Docker-in-Docker: use Docker volume paths on host // The HOST_*_DIR tells the challenge how to map container paths to host paths for DinD - env.push("HOST_TASKS_DIR=/var/lib/docker/volumes/term-challenge-tasks/_data".to_string()); - env.push("HOST_CACHE_DIR=/var/lib/docker/volumes/term-challenge-cache/_data".to_string()); - env.push("CACHE_DIR=/root/.cache/term-challenge".to_string()); + env.push("HOST_TASKS_DIR=/var/lib/docker/volumes/challenge-tasks/_data".to_string()); + env.push("HOST_CACHE_DIR=/var/lib/docker/volumes/challenge-cache/_data".to_string()); + env.push("CACHE_DIR=/root/.cache/challenge".to_string()); env.push( - "HOST_BENCHMARK_RESULTS_DIR=/var/lib/docker/volumes/term-challenge-evals/_data" + "HOST_BENCHMARK_RESULTS_DIR=/var/lib/docker/volumes/challenge-evals/_data" .to_string(), ); - env.push("BENCHMARK_RESULTS_DIR=/tmp/term-challenge-evals".to_string()); + env.push("BENCHMARK_RESULTS_DIR=/tmp/challenge-evals".to_string()); // Pass through DEVELOPMENT_MODE for local image support if let Ok(dev_mode) = std::env::var("DEVELOPMENT_MODE") { env.push(format!("DEVELOPMENT_MODE={}", dev_mode)); @@ -1137,7 +1137,7 @@ impl DockerClient { /// - Watchtower containers /// /// Parameters: - /// - `prefix`: Container name prefix to match (e.g., "term-challenge-") + /// - `prefix`: Container name prefix to match (e.g., "challenge-task-") /// - `max_age_minutes`: Only remove containers older than this (0 = remove all matching) /// - `exclude_patterns`: Container names containing these patterns will be kept pub async fn cleanup_stale_containers( @@ -1572,14 +1572,14 @@ mod tests { let bridge = RecordingBridge::default(); let now = chrono::Utc::now().timestamp(); bridge.set_containers(vec![ - make_container_summary("old", "term-challenge-old", now - 10_000), + make_container_summary("old", "challenge-task-old", now - 10_000), make_container_summary("exclude", "platform-helper", now - 10_000), - make_container_summary("young", "term-challenge-young", now - 100), + make_container_summary("young", "challenge-task-young", now - 100), ]); let client = DockerClient::with_bridge(bridge.clone(), "platform-network"); let result = client - .cleanup_stale_containers("term-challenge-", 120, &["platform-"]) + .cleanup_stale_containers("challenge-task-", 120, &["platform-"]) .await .unwrap(); assert_eq!(result.total_found, 1); diff --git a/crates/challenge-orchestrator/src/evaluator.rs b/crates/challenge-orchestrator/src/evaluator.rs index 85647ffc..0b01b217 100644 --- a/crates/challenge-orchestrator/src/evaluator.rs +++ b/crates/challenge-orchestrator/src/evaluator.rs @@ -5,7 +5,7 @@ //! timeouts, and surfaces useful errors back to the validator. //! //! For challenge-specific schemas, see each challenge repository (for example, -//! `term-challenge-repo/src/server.rs`). +//! `the challenge repository`). use crate::{ChallengeInstance, ContainerStatus}; use parking_lot::RwLock; @@ -296,13 +296,13 @@ mod tests { #[test] fn test_challenge_info_deserialize() { let json = r#"{ - "name": "term-challenge", + "name": "test-challenge", "version": "1.0.0", "description": "Terminal benchmark challenge" }"#; let info: ChallengeInfo = serde_json::from_str(json).unwrap(); - assert_eq!(info.name, "term-challenge"); + assert_eq!(info.name, "test-challenge"); assert_eq!(info.mechanism_id, 0); // default } diff --git a/crates/challenge-orchestrator/src/lib.rs b/crates/challenge-orchestrator/src/lib.rs index 20aa7e99..5d3d7597 100644 --- a/crates/challenge-orchestrator/src/lib.rs +++ b/crates/challenge-orchestrator/src/lib.rs @@ -316,16 +316,16 @@ impl ChallengeOrchestrator { #[deprecated(note = "Docker-based container cleanup is deprecated; prefer WASM-based challenge execution")] pub async fn cleanup_stale_task_containers(&self) -> anyhow::Result { tracing::warn!("Docker-based container cleanup is deprecated; prefer WASM-based challenge execution"); - // Clean up term-challenge task containers older than 2 hours + // Clean up challenge task containers older than 2 hours // Exclude: // - challenge-* (main challenge containers managed by orchestrator) // - platform-* (validator, watchtower) let result = self .docker .cleanup_stale_containers( - "term-challenge-", + "challenge-task-", 120, // 2 hours old - &["challenge-term-challenge", "platform-"], + &["challenge-", "platform-"], ) .await?; @@ -706,10 +706,10 @@ mod tests { let calls = docker.cleanup_calls(); assert_eq!(calls.len(), 1); let (prefix, max_age, excludes) = &calls[0]; - assert_eq!(prefix, "term-challenge-"); + assert_eq!(prefix, "challenge-task-"); assert_eq!(*max_age, 120); let expected: Vec = vec![ - "challenge-term-challenge".to_string(), + "challenge-".to_string(), "platform-".to_string(), ]; assert_eq!(excludes, &expected); diff --git a/crates/challenge-registry/src/registry.rs b/crates/challenge-registry/src/registry.rs index 88e41295..ba1a99d6 100644 --- a/crates/challenge-registry/src/registry.rs +++ b/crates/challenge-registry/src/registry.rs @@ -25,7 +25,7 @@ pub struct WasmModuleMetadata { /// Network policy for WASM execution #[serde(default)] pub network_policy: NetworkPolicy, - /// Sandbox policy for term-challenge execution + /// Sandbox policy for challenge execution #[serde(default)] pub sandbox_policy: Option, /// Restartable configuration identifier diff --git a/crates/challenge-sdk-wasm/src/lib.rs b/crates/challenge-sdk-wasm/src/lib.rs index 796552e9..820c7d4f 100644 --- a/crates/challenge-sdk-wasm/src/lib.rs +++ b/crates/challenge-sdk-wasm/src/lib.rs @@ -5,14 +5,11 @@ extern crate alloc; pub mod alloc_impl; pub mod host_functions; pub mod llm_types; -pub mod term_types; pub mod types; pub use llm_types::{LlmMessage, LlmRequest, LlmResponse, LlmUsage}; -pub use term_types::*; pub use types::{ score_f64_scaled, SandboxExecRequest, SandboxExecResponse, TaskDefinition, TaskResult, - TermEvaluationParams, }; pub use types::{ContainerRunRequest, ContainerRunResponse}; pub use types::{EvaluationInput, EvaluationOutput}; diff --git a/crates/challenge-sdk-wasm/src/term_types.rs b/crates/challenge-sdk-wasm/src/term_types.rs deleted file mode 100644 index bed1d66d..00000000 --- a/crates/challenge-sdk-wasm/src/term_types.rs +++ /dev/null @@ -1,123 +0,0 @@ -use alloc::string::String; -use alloc::vec::Vec; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct CommandRequest { - pub command: String, - pub args: Vec, - pub env_vars: Vec<(String, String)>, - pub working_dir: Option, - pub timeout_ms: u64, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct CommandResult { - pub exit_code: i32, - pub stdout: Vec, - pub stderr: Vec, - pub execution_time_ms: u64, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct FileReadRequest { - pub path: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct FileReadResponse { - pub data: Vec, - pub success: bool, - pub error: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct FileWriteRequest { - pub path: String, - pub data: Vec, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct FileWriteResponse { - pub success: bool, - pub bytes_written: u64, - pub error: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct FileListRequest { - pub path: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct FileListResponse { - pub entries: Vec, - pub success: bool, - pub error: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct FileEntry { - pub name: String, - pub is_dir: bool, - pub size: u64, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct TaskDefinition { - pub task_id: String, - pub description: String, - pub expected_output_hash: Option>, - pub environment_config: Vec, - pub scoring_params: Vec, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct TermEvaluationMetrics { - pub execution_time_ms: u64, - pub correctness_score: f64, - pub partial_credit: f64, - pub cost: f64, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct TermEvaluationInput { - pub agent_data: Vec, - pub challenge_id: String, - pub params: Vec, - pub task_definition: Option>, - pub environment_config: Option>, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct TermEvaluationOutput { - pub score: i64, - pub valid: bool, - pub message: String, - pub metrics: Option>, -} - -impl TermEvaluationOutput { - pub fn success(score: i64, message: &str) -> Self { - Self { - score, - valid: true, - message: String::from(message), - metrics: None, - } - } - - pub fn failure(message: &str) -> Self { - Self { - score: 0, - valid: false, - message: String::from(message), - metrics: None, - } - } - - pub fn with_metrics(mut self, metrics: Vec) -> Self { - self.metrics = Some(metrics); - self - } -} diff --git a/crates/challenge-sdk-wasm/src/types.rs b/crates/challenge-sdk-wasm/src/types.rs index 9c8adeb3..3c3763fb 100644 --- a/crates/challenge-sdk-wasm/src/types.rs +++ b/crates/challenge-sdk-wasm/src/types.rs @@ -121,14 +121,6 @@ impl TaskResult { } } -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct TermEvaluationParams { - pub challenge_id: String, - pub task_definitions: Vec, - pub timeout_ms: u64, - pub environment_config: Option>, -} - #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ContainerRunRequest { pub image: String, diff --git a/crates/secure-container-runtime/src/policy.rs b/crates/secure-container-runtime/src/policy.rs index 5b9a2546..db4cd1b4 100644 --- a/crates/secure-container-runtime/src/policy.rs +++ b/crates/secure-container-runtime/src/policy.rs @@ -341,7 +341,7 @@ mod tests { fn test_validate_image_allowed() { let policy = SecurityPolicy::default(); assert!(policy - .validate_image("ghcr.io/platformnetwork/term-challenge:latest") + .validate_image("ghcr.io/platformnetwork/test-challenge:latest") .is_ok()); assert!(policy.validate_image("platform-challenge:latest").is_ok()); } @@ -401,7 +401,7 @@ mod tests { let policy = SecurityPolicy::default(); let config = ContainerConfig { - image: "ghcr.io/platformnetwork/term-challenge:latest".to_string(), + image: "ghcr.io/platformnetwork/test-challenge:latest".to_string(), challenge_id: "test-challenge".to_string(), owner_id: "test-owner".to_string(), ..Default::default() @@ -415,7 +415,7 @@ mod tests { let policy = SecurityPolicy::default(); let config = ContainerConfig { - image: "ghcr.io/platformnetwork/term-challenge:latest".to_string(), + image: "ghcr.io/platformnetwork/test-challenge:latest".to_string(), challenge_id: "".to_string(), owner_id: "test-owner".to_string(), ..Default::default() diff --git a/crates/secure-container-runtime/src/ws_transport.rs b/crates/secure-container-runtime/src/ws_transport.rs index 7575d97f..c013f54e 100644 --- a/crates/secure-container-runtime/src/ws_transport.rs +++ b/crates/secure-container-runtime/src/ws_transport.rs @@ -403,14 +403,14 @@ mod tests { #[test] fn test_generate_and_verify_token() { let secret = "test-secret-key-123"; - let token = generate_token("term-challenge", "validator-1", secret, 3600).unwrap(); + let token = generate_token("test-challenge", "validator-1", secret, 3600).unwrap(); // Verify token let key = jsonwebtoken::DecodingKey::from_secret(secret.as_bytes()); let validation = jsonwebtoken::Validation::default(); let decoded = jsonwebtoken::decode::(&token, &key, &validation).unwrap(); - assert_eq!(decoded.claims.challenge_id, "term-challenge"); + assert_eq!(decoded.claims.challenge_id, "test-challenge"); assert_eq!(decoded.claims.owner_id, "validator-1"); } diff --git a/crates/secure-container-runtime/tests/integration_tests.rs b/crates/secure-container-runtime/tests/integration_tests.rs index dad1dd73..925efaf8 100644 --- a/crates/secure-container-runtime/tests/integration_tests.rs +++ b/crates/secure-container-runtime/tests/integration_tests.rs @@ -10,7 +10,7 @@ use secure_container_runtime::*; use std::time::Duration; use tokio::time::sleep; -const TEST_IMAGE: &str = "ghcr.io/platformnetwork/term-challenge:latest"; +const TEST_IMAGE: &str = "ghcr.io/platformnetwork/test-challenge:latest"; const MALICIOUS_IMAGE: &str = "docker.io/malicious/evil:latest"; // ============================================================================ @@ -59,7 +59,7 @@ fn test_strict_policy_blocks_non_whitelisted_images() { // Allowed images (in whitelist) let allowed = vec![ - "ghcr.io/platformnetwork/term-challenge:latest", + "ghcr.io/platformnetwork/test-challenge:latest", "ghcr.io/platformnetwork/validator:v1.0.0", "GHCR.IO/PLATFORMNETWORK/test:latest", // Case insensitive ]; @@ -79,7 +79,7 @@ fn test_permissive_policy_allows_all_images() { "alpine:latest", "ubuntu:22.04", "alexgshaw/code-from-image:20251031", - "ghcr.io/platformnetwork/term-challenge:latest", + "ghcr.io/platformnetwork/test-challenge:latest", ]; for image in images { @@ -95,7 +95,7 @@ fn test_default_policy_allows_whitelisted_images() { // Platform images should be allowed let allowed_images = vec![ - "ghcr.io/platformnetwork/term-challenge:latest", + "ghcr.io/platformnetwork/test-challenge:latest", "platform-compiler:latest", ]; diff --git a/crates/wasm-runtime-interface/src/lib.rs b/crates/wasm-runtime-interface/src/lib.rs index 1f8f9e41..f5abf2a8 100644 --- a/crates/wasm-runtime-interface/src/lib.rs +++ b/crates/wasm-runtime-interface/src/lib.rs @@ -105,7 +105,7 @@ pub struct NetworkPolicy { pub audit: AuditPolicy, } -/// Sandbox policy for term-challenge WASM modules. +/// Sandbox policy for challenge WASM modules. /// /// Controls whether sandbox command execution is permitted and enforces /// resource limits on spawned processes. @@ -139,8 +139,8 @@ impl SandboxPolicy { } } - /// Term-challenge default sandbox policy. - pub fn term_challenge() -> Self { + /// Default challenge sandbox policy. + pub fn default_challenge() -> Self { Self { enable_sandbox: true, allowed_commands: vec![ diff --git a/crates/wasm-runtime-interface/src/runtime.rs b/crates/wasm-runtime-interface/src/runtime.rs index 38c5939b..922be59e 100644 --- a/crates/wasm-runtime-interface/src/runtime.rs +++ b/crates/wasm-runtime-interface/src/runtime.rs @@ -97,7 +97,7 @@ impl Default for RuntimeConfig { pub struct InstanceConfig { /// Network policy enforced by host functions. pub network_policy: NetworkPolicy, - /// Sandbox policy for term-challenge execution. + /// Sandbox policy for challenge execution. pub sandbox_policy: SandboxPolicy, /// Exec policy enforced by host functions. pub exec_policy: ExecPolicy, @@ -164,7 +164,7 @@ impl Default for InstanceConfig { pub struct RuntimeState { /// Network policy available to host functions. pub network_policy: NetworkPolicy, - /// Sandbox policy for term-challenge execution. + /// Sandbox policy for challenge execution. pub sandbox_policy: SandboxPolicy, /// Mutable network state enforcing policy. pub network_state: NetworkState, diff --git a/crates/wasm-runtime-interface/src/sandbox.rs b/crates/wasm-runtime-interface/src/sandbox.rs index fbef56f9..4e3210bd 100644 --- a/crates/wasm-runtime-interface/src/sandbox.rs +++ b/crates/wasm-runtime-interface/src/sandbox.rs @@ -661,8 +661,8 @@ mod tests { } #[test] - fn test_sandbox_policy_term_challenge() { - let policy = SandboxPolicy::term_challenge(); + fn test_sandbox_policy_default_challenge() { + let policy = SandboxPolicy::default_challenge(); assert!(policy.enable_sandbox); assert!(policy.allowed_commands.contains(&"bash".to_string())); assert!(policy.allowed_commands.contains(&"python3".to_string())); diff --git a/crates/wasm-runtime-interface/src/terminal.rs b/crates/wasm-runtime-interface/src/terminal.rs index a1ffde23..ad5c65eb 100644 --- a/crates/wasm-runtime-interface/src/terminal.rs +++ b/crates/wasm-runtime-interface/src/terminal.rs @@ -110,7 +110,7 @@ impl TerminalPolicy { } } - pub fn term_challenge() -> Self { + pub fn default_challenge() -> Self { Self { enabled: true, allowed_commands: vec![ @@ -689,7 +689,7 @@ mod tests { #[test] fn test_terminal_policy_path_check() { - let policy = TerminalPolicy::term_challenge(); + let policy = TerminalPolicy::default_challenge(); assert!(policy.is_path_allowed("/tmp/test.txt")); assert!(!policy.is_path_allowed("/etc/passwd")); } diff --git a/docker/Dockerfile.challenge b/docker/Dockerfile.challenge index 10da5f71..06e7a6f1 100644 --- a/docker/Dockerfile.challenge +++ b/docker/Dockerfile.challenge @@ -3,9 +3,9 @@ # # Usage: # docker build -f docker/Dockerfile.challenge \ -# --build-arg CHALLENGE_NAME=term-challenge \ -# --build-arg CHALLENGE_DIR=challenges/term-challenge \ -# -t cortexlm/challenge-term-bench:v1.0.0 . +# --build-arg CHALLENGE_NAME=my-challenge \ +# --build-arg CHALLENGE_DIR=challenges/my-challenge \ +# -t cortexlm/challenge-my-challenge:v1.0.0 . # ===== Build Stage ===== FROM rust:1.92-bookworm as builder @@ -17,8 +17,8 @@ ENV PLATFORM_NIGHTLY_RUSTFLAGS=${PLATFORM_NIGHTLY_RUSTFLAGS} ENV PLATFORM_LINKER_RUSTFLAGS=${PLATFORM_LINKER_RUSTFLAGS} ENV INSTALL_FAST_LINKER=${INSTALL_FAST_LINKER} -ARG CHALLENGE_NAME=term-challenge -ARG CHALLENGE_DIR=challenges/term-challenge +ARG CHALLENGE_NAME=my-challenge +ARG CHALLENGE_DIR=challenges/my-challenge WORKDIR /app @@ -50,7 +50,7 @@ RUN cargo build --release # ===== Runtime Stage ===== FROM debian:bookworm-slim -ARG CHALLENGE_NAME=term-challenge +ARG CHALLENGE_NAME=my-challenge # Install runtime dependencies RUN apt-get update && apt-get install -y \ diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 70031c31..3ecb47b8 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,9 +1,9 @@ -# Mini-Chain Validator Docker Compose +# Platform Validator Docker Compose # For local development and testing # -# Challenges are Rust-based and implement the Challenge trait from challenge-sdk. -# Each challenge runs in its own container and communicates with the validator -# via the minichain Docker network. +# Challenges are maintained in separate repositories and import +# platform-challenge-sdk as a git dependency. Add your challenge +# service below following the example template. version: '3.8' @@ -13,7 +13,7 @@ services: build: context: .. dockerfile: docker/Dockerfile.validator - container_name: mini-chain-validator + container_name: platform-validator restart: unless-stopped environment: - VALIDATOR_SECRET_KEY=${VALIDATOR_SECRET_KEY} @@ -28,50 +28,43 @@ services: - "9000:9000" # P2P - "8080:8080" # RPC networks: - - minichain + - platform healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s timeout: 10s retries: 3 start_period: 30s - depends_on: - - challenge-term-bench - # ===== Terminal Benchmark Challenge ===== - # Rust-based challenge implementing the Challenge trait - challenge-term-bench: - build: - context: ../.. - dockerfile: mini-chain/docker/Dockerfile.challenge - args: - CHALLENGE_NAME: term-challenge - CHALLENGE_DIR: challenges/term-challenge - image: cortexlm/challenge-term-bench:${TERM_BENCH_VERSION:-latest} - container_name: challenge-term-bench - restart: unless-stopped - environment: - - RUST_LOG=info - - CHALLENGE_PORT=8080 - - MECHANISM_ID=1 - - EMISSION_WEIGHT=1.0 - volumes: - - term-bench-data:/data - - /var/run/docker.sock:/var/run/docker.sock - networks: - - minichain - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health"] - interval: 10s - timeout: 5s - retries: 3 - start_period: 30s + # ===== Example Challenge (uncomment and customise) ===== + # challenge-example: + # build: + # context: ../.. + # dockerfile: docker/Dockerfile.challenge + # args: + # CHALLENGE_NAME: my-challenge + # CHALLENGE_DIR: challenges/my-challenge + # image: your-org/challenge-example:latest + # container_name: challenge-example + # restart: unless-stopped + # environment: + # - RUST_LOG=info + # - CHALLENGE_PORT=8080 + # volumes: + # - challenge-data:/data + # networks: + # - platform + # healthcheck: + # test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + # interval: 10s + # timeout: 5s + # retries: 3 + # start_period: 30s networks: - minichain: + platform: driver: bridge - name: minichain + name: platform volumes: validator-data: - term-bench-data: diff --git a/docs/validator_wasm_audit.md b/docs/validator_wasm_audit.md index c52a49a0..c6f61c14 100644 --- a/docs/validator_wasm_audit.md +++ b/docs/validator_wasm_audit.md @@ -16,7 +16,7 @@ Reviewed: `bins/validator-node`, `crates/core`, `crates/p2p-consensus`, `crates/ - Docker-only notions appear in: - `ChallengeContainerConfig` usage (core) and `ChallengeInstance` container ID/endpoint metadata. - `refresh_challenge` uses a synthesized config with hardcoded defaults (mechanism_id=0, timeout, CPU/mem). This bypasses canonical config/state. - - `cleanup_stale_task_containers` has hardcoded `term-challenge-` prefixes and container exclusions. + - `cleanup_stale_task_containers` has hardcoded challenge container prefixes and container exclusions. ### Challenge Registry - Registry entries still require a `docker_image` field and only optionally include WASM metadata. This is a legacy docker-first structure that needs inversion for WASM-first. @@ -48,7 +48,7 @@ Reviewed: `bins/validator-node`, `crates/core`, `crates/p2p-consensus`, `crates/ - `ChallengeEntry` should store WASM module metadata as primary, with docker fields removed or optional legacy for migration. - `discovery` should focus on WASM module registry or signed P2P announcements instead of docker registry scanning. 5. **Remove hardcoded challenge Docker names** - - `cleanup_stale_task_containers` in `challenge-orchestrator` has hardcoded `term-challenge-` and should be removed or generalized. + - `cleanup_stale_task_containers` in `challenge-orchestrator` has hardcoded container prefixes and should be removed or generalized. 6. **Consensus state challenge metadata** - Replace `p2p-consensus::ChallengeConfig` docker image with WASM module metadata (hash/path/entrypoint/policy) to support WASM-only evaluation. diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh index a1daee3b..cafaa63d 100755 --- a/scripts/build-wasm.sh +++ b/scripts/build-wasm.sh @@ -69,11 +69,6 @@ build_challenge() { if [ -n "$1" ]; then build_challenge "$1" else - # Build term-challenge-wasm if it exists as a workspace member - if cargo metadata --no-deps --format-version 1 2>/dev/null | grep -q '"name":"term-challenge-wasm"'; then - build_challenge "term-challenge-wasm" - fi - # Build any challenge crates found under challenges/*/ for dir in challenges/*/; do if [ -f "${dir}Cargo.toml" ]; then From 9851ead19e3de5bab53924cdbeefd47c6d455cc2 Mon Sep 17 00:00:00 2001 From: echobt Date: Wed, 18 Feb 2026 18:40:07 +0000 Subject: [PATCH 2/5] ci: trigger CI checks From fedcae31ccb7536f157b5edfaaffa9b415df087e Mon Sep 17 00:00:00 2001 From: echobt Date: Wed, 18 Feb 2026 18:46:26 +0000 Subject: [PATCH 3/5] fix(security): patch path traversal, credential leak, and input validation - terminal.rs: Block path traversal in is_path_allowed() by rejecting paths containing '..' and normalizing before prefix check (HIGH) - docker.rs: Remove forwarding of VALIDATOR_SECRET_KEY to challenge containers to prevent credential exposure to third-party code (MEDIUM) - main.rs: Add validate_github_repo() to prevent URL path injection via malicious config values in GitHub API URLs (MEDIUM) --- bins/platform-cli/src/main.rs | 34 +++++++++++++++++++ crates/challenge-orchestrator/src/docker.rs | 7 ++-- crates/wasm-runtime-interface/src/terminal.rs | 31 ++++++++++++++++- 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/bins/platform-cli/src/main.rs b/bins/platform-cli/src/main.rs index 960ca10e..c054758d 100644 --- a/bins/platform-cli/src/main.rs +++ b/bins/platform-cli/src/main.rs @@ -252,10 +252,44 @@ fn find_matching_asset(assets: &[GitHubAsset]) -> Option<&GitHubAsset> { // ==================== GitHub API ==================== +/// Validate that a GitHub repo string is in the expected `owner/repo` format. +/// +/// Prevents URL path injection when the value is interpolated into API URLs. +/// Only alphanumeric characters, hyphens, underscores, and dots are permitted +/// in each segment. +fn validate_github_repo(repo: &str) -> Result<()> { + let parts: Vec<&str> = repo.split('/').collect(); + if parts.len() != 2 { + anyhow::bail!( + "Invalid github_repo '{}': must be in 'owner/repo' format", + repo + ); + } + for part in &parts { + if part.is_empty() { + anyhow::bail!( + "Invalid github_repo '{}': owner and repo must not be empty", + repo + ); + } + if !part + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.') + { + anyhow::bail!( + "Invalid github_repo '{}': contains disallowed characters", + repo + ); + } + } + Ok(()) +} + async fn fetch_latest_release( client: &reqwest::Client, github_repo: &str, ) -> Result { + validate_github_repo(github_repo)?; let url = format!("{}/repos/{}/releases/latest", GITHUB_API_BASE, github_repo); debug!("Fetching latest release from {}", url); diff --git a/crates/challenge-orchestrator/src/docker.rs b/crates/challenge-orchestrator/src/docker.rs index 2f0f6a8d..d1717e29 100644 --- a/crates/challenge-orchestrator/src/docker.rs +++ b/crates/challenge-orchestrator/src/docker.rs @@ -844,10 +844,9 @@ impl DockerClient { if let Ok(validator_hotkey) = std::env::var("VALIDATOR_HOTKEY") { env.push(format!("VALIDATOR_HOTKEY={}", validator_hotkey)); } - // Pass validator secret key for signing requests (needed by challenge validator workers) - if let Ok(validator_secret) = std::env::var("VALIDATOR_SECRET_KEY") { - env.push(format!("VALIDATOR_SECRET={}", validator_secret)); - } + // SECURITY: VALIDATOR_SECRET_KEY is intentionally NOT forwarded to challenge + // containers. Challenge containers are third-party code and must never receive + // the validator's private signing key. // Pass owner/sudo hotkey for challenge sudo operations if let Ok(owner_hotkey) = std::env::var("OWNER_HOTKEY") { env.push(format!("OWNER_HOTKEY={}", owner_hotkey)); diff --git a/crates/wasm-runtime-interface/src/terminal.rs b/crates/wasm-runtime-interface/src/terminal.rs index ad5c65eb..88775bd1 100644 --- a/crates/wasm-runtime-interface/src/terminal.rs +++ b/crates/wasm-runtime-interface/src/terminal.rs @@ -140,10 +140,30 @@ impl TerminalPolicy { if !self.enabled { return false; } + if path.contains("..") { + return false; + } + let normalized = std::path::Path::new(path).components().fold( + std::path::PathBuf::new(), + |mut acc, comp| { + match comp { + std::path::Component::ParentDir => { + acc.pop(); + } + std::path::Component::Normal(s) => acc.push(s), + std::path::Component::RootDir => acc.push("/"), + _ => {} + } + acc + }, + ); + let normalized_str = normalized.to_string_lossy(); if self.allowed_paths.is_empty() { return true; } - self.allowed_paths.iter().any(|p| path.starts_with(p)) + self.allowed_paths + .iter() + .any(|p| normalized_str.starts_with(p)) } } @@ -694,6 +714,15 @@ mod tests { assert!(!policy.is_path_allowed("/etc/passwd")); } + #[test] + fn test_terminal_policy_blocks_path_traversal() { + let policy = TerminalPolicy::default_challenge(); + assert!(!policy.is_path_allowed("/tmp/../../etc/passwd")); + assert!(!policy.is_path_allowed("/tmp/../etc/shadow")); + assert!(!policy.is_path_allowed("/tmp/safe/../../root/.ssh/id_rsa")); + assert!(!policy.is_path_allowed("/tmp/..")); + } + #[test] fn test_terminal_policy_disabled_blocks_all() { let policy = TerminalPolicy::default(); From a3cd8e5b3db0ce3cb7020777d3eaecd9c7c4d680 Mon Sep 17 00:00:00 2001 From: echobt Date: Wed, 18 Feb 2026 18:46:42 +0000 Subject: [PATCH 4/5] fix(quality): remove .expect() in non-test code and TODO comment - Replace .expect() with .unwrap_or_else() fallback in challenge-orchestrator evaluator.rs and health.rs - Remove TODO comment from Cargo.toml clippy config --- Cargo.toml | 2 +- crates/challenge-orchestrator/src/evaluator.rs | 5 ++++- crates/challenge-orchestrator/src/health.rs | 5 ++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c6805bef..8f423ce8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,7 +100,7 @@ w3f-bls = { git = "https://github.com/opentensor/bls", branch = "fix-no-std" } too_many_arguments = "allow" large_enum_variant = "allow" type_complexity = "allow" -await_holding_lock = "warn" # TODO: Fix async lock issues properly +await_holding_lock = "warn" collapsible_match = "allow" collapsible_if = "allow" diff --git a/crates/challenge-orchestrator/src/evaluator.rs b/crates/challenge-orchestrator/src/evaluator.rs index 0b01b217..ee029934 100644 --- a/crates/challenge-orchestrator/src/evaluator.rs +++ b/crates/challenge-orchestrator/src/evaluator.rs @@ -28,7 +28,10 @@ impl ChallengeEvaluator { let client = reqwest::Client::builder() .timeout(Duration::from_secs(3600)) .build() - .expect("Failed to create HTTP client"); + .unwrap_or_else(|e| { + warn!("Failed to create HTTP client with custom config: {e}; using defaults"); + reqwest::Client::new() + }); Self { challenges, client } } diff --git a/crates/challenge-orchestrator/src/health.rs b/crates/challenge-orchestrator/src/health.rs index d289089a..2d9a6f65 100644 --- a/crates/challenge-orchestrator/src/health.rs +++ b/crates/challenge-orchestrator/src/health.rs @@ -24,7 +24,10 @@ impl HealthMonitor { let client = reqwest::Client::builder() .timeout(Duration::from_secs(5)) .build() - .expect("Failed to create HTTP client"); + .unwrap_or_else(|e| { + warn!("Failed to create HTTP client with custom config: {e}; using defaults"); + reqwest::Client::new() + }); Self { challenges, From d502a012f0dc5bb290c46590c96e4a45ac16648a Mon Sep 17 00:00:00 2001 From: echobt Date: Wed, 18 Feb 2026 18:59:34 +0000 Subject: [PATCH 5/5] docs: refresh AGENTS.md --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index 0c401dbf..e9cf0cd7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -173,6 +173,7 @@ flowchart TB - `crates/p2p-consensus` — libp2p gossipsub + DHT consensus - `crates/wasm-runtime-interface` — WASM runtime host interface - `bins/validator-node` — main validator binary +- `bins/platform-cli` — CLI for downloading and managing challenge CLIs - `bins/utils` — CLI utilities - `bins/mock-subtensor` — mock Bittensor node for testing - `tests` — integration tests