From 5a4b5b83dd1af8c210924d2ec89f031836d89ed5 Mon Sep 17 00:00:00 2001 From: aguitauwu Date: Mon, 16 Feb 2026 11:41:18 -0600 Subject: [PATCH] pos nomas --- .gitignore | 12 ++ Dockerfile | 21 +++ LICENSE | 202 +------------------------ Makefile | 20 +++ build.sh | 25 ++++ docker-compose.yml | 11 ++ examples/api_usage.py | 15 ++ examples/basic_usage.py | 19 +++ install.sh | 23 +++ pyproject.toml | 40 +++++ tests/test_executor.py | 32 ++++ yuubox-core/Cargo.toml | 24 +++ yuubox-core/src/container.rs | 222 ++++++++++++++++++++++++++++ yuubox-core/src/lib.rs | 96 ++++++++++++ yuubox-core/src/limits.rs | 16 ++ yuubox-core/src/monitor.rs | 17 +++ yuubox-core/tests/test_container.rs | 7 + yuubox/__init__.py | 15 ++ yuubox/analyzer.py | 59 ++++++++ yuubox/api.py | 42 ++++++ yuubox/cli.py | 69 +++++++++ yuubox/exceptions.py | 15 ++ yuubox/executor.py | 128 ++++++++++++++++ yuubox/healer.py | 90 +++++++++++ 24 files changed, 1021 insertions(+), 199 deletions(-) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100755 build.sh create mode 100644 docker-compose.yml create mode 100644 examples/api_usage.py create mode 100644 examples/basic_usage.py create mode 100755 install.sh create mode 100644 pyproject.toml create mode 100644 tests/test_executor.py create mode 100644 yuubox-core/Cargo.toml create mode 100644 yuubox-core/src/container.rs create mode 100644 yuubox-core/src/lib.rs create mode 100644 yuubox-core/src/limits.rs create mode 100644 yuubox-core/src/monitor.rs create mode 100644 yuubox-core/tests/test_container.rs create mode 100644 yuubox/__init__.py create mode 100644 yuubox/analyzer.py create mode 100644 yuubox/api.py create mode 100644 yuubox/cli.py create mode 100644 yuubox/exceptions.py create mode 100644 yuubox/executor.py create mode 100644 yuubox/healer.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a5aa81b --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +target/ +**/*.rs.bk +*.pyc +__pycache__/ +*.so +*.pyd +.pytest_cache/ +dist/ +build/ +*.egg-info/ +.env +.venv/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4418c89 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM rust:1.75 as builder + +WORKDIR /build +COPY yuubox-core ./yuubox-core +COPY pyproject.toml ./ + +RUN apt-get update && apt-get install -y python3-dev python3-pip +RUN pip3 install maturin +RUN maturin build --release --manifest-path yuubox-core/Cargo.toml + +FROM python:3.11-slim + +WORKDIR /app +COPY --from=builder /build/target/wheels/*.whl ./ +COPY yuubox ./yuubox +COPY pyproject.toml ./ + +RUN pip install *.whl +RUN pip install ".[api]" + +CMD ["uvicorn", "yuubox.api:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/LICENSE b/LICENSE index 261eeb9..1b6f6cb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,5 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +Apache License 2.0 - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +Copyright 2026 Yuuki Project - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Licensed under the Apache License, Version 2.0 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..620f4cf --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +.PHONY: build dev install test clean + +build: + maturin build --release + +dev: + maturin develop + +install: + pip install -e ".[dev]" + +test: + pytest tests/ + cargo test --manifest-path yuubox-core/Cargo.toml + +clean: + rm -rf target dist build *.egg-info + find . -type d -name __pycache__ -exec rm -rf {} + + find . -type f -name "*.pyc" -delete + find . -type f -name "*.so" -delete diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..4abb964 --- /dev/null +++ b/build.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -e + +echo "Building YuuBox (Rust + Python)" +echo "" + +echo "Step 1: Build Rust core..." +cd yuubox-core +cargo build --release +cd .. + +echo "" +echo "Step 2: Build Python wheel with maturin..." +maturin build --release + +echo "" +echo "Step 3: Install locally..." +pip install target/wheels/*.whl + +echo "" +echo "✅ Build complete!" +echo "" +echo "Usage:" +echo " yuubox run script.py" +echo " python -c 'from yuubox import YuuBox; box = YuuBox()'" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5c5361a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +version: '3.8' + +services: + yuubox-api: + build: . + ports: + - "8000:8000" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + - YUUKI_API_URL=https://opceanai-yuuki-api.hf.space diff --git a/examples/api_usage.py b/examples/api_usage.py new file mode 100644 index 0000000..a3bb3d8 --- /dev/null +++ b/examples/api_usage.py @@ -0,0 +1,15 @@ +from yuubox import YuuBox, ResourceLimits + +box = YuuBox(max_iterations=5) + +limits = ResourceLimits( + memory_mb=512, + cpu_quota=1.0, + timeout_seconds=30, +) + +code = "print('Hello from YuuBox!')" +result = box.execute(code, "python", limits=limits) + +print(f"Success: {result.success}") +print(f"Output: {result.stdout}") diff --git a/examples/basic_usage.py b/examples/basic_usage.py new file mode 100644 index 0000000..7461461 --- /dev/null +++ b/examples/basic_usage.py @@ -0,0 +1,19 @@ +from yuubox import YuuBox + +box = YuuBox() + +code = """ +def fibonacci(n): + if n <= 1: + return n + return fibonacci(n-1) + fibonacci(n-2) + +# Typo: fibonaci vs fibonacci +print(fibonaci(10)) +""" + +result = box.execute(code, language="python") + +print(f"Success: {result.success}") +print(f"Iterations: {result.iterations}") +print(f"Output: {result.stdout}") diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..a9d68eb --- /dev/null +++ b/install.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -e + +echo "Installing YuuBox..." + +# Install Rust if not present +if ! command -v cargo &> /dev/null; then + echo "Installing Rust..." + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + source $HOME/.cargo/env +fi + +# Install maturin +pip install maturin + +# Build and install +./build.sh + +echo "" +echo "✅ Installation complete!" +echo "" +echo "Test with:" +echo " yuubox run examples/basic_usage.py" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5f02dbf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,40 @@ +[build-system] +requires = ["maturin>=1.4,<2.0"] +build-backend = "maturin" + +[project] +name = "yuubox" +version = "1.0.0" +description = "Self-Healing Code Execution System (Rust + Python)" +requires-python = ">=3.9" +license = {text = "Apache-2.0"} +dependencies = [ + "rich>=13.0.0", + "click>=8.0.0", + "requests>=2.28.0", + "pyyaml>=6.0", +] + +[project.optional-dependencies] +api = [ + "fastapi>=0.104.0", + "uvicorn[standard]>=0.24.0", +] +dev = [ + "pytest>=7.4.0", + "black>=23.0.0", + "maturin>=1.4", +] +all = ["yuubox[api,dev]"] + +[project.scripts] +yuubox = "yuubox.cli:main" + +[tool.maturin] +python-source = "." +module-name = "yuubox.yuubox_core" +bindings = "pyo3" +manifest-path = "yuubox-core/Cargo.toml" + +[tool.black] +line-length = 100 diff --git a/tests/test_executor.py b/tests/test_executor.py new file mode 100644 index 0000000..a90a547 --- /dev/null +++ b/tests/test_executor.py @@ -0,0 +1,32 @@ +import pytest +from yuubox import YuuBox, ResourceLimits + +def test_basic_execution(): + """Test basic code execution""" + box = YuuBox(max_iterations=1) + + code = 'print("hello world")' + result = box.execute(code, language="python", no_healing=True) + + assert "hello world" in result.stdout + +def test_self_healing(): + """Test self-healing capability""" + box = YuuBox(max_iterations=5) + + # Code with intentional error (typo) + code = 'prin("hello")' # prin vs print + result = box.execute(code, language="python") + + # Should eventually succeed after healing + assert result.iterations >= 1 + +def test_resource_limits(): + """Test resource limits work""" + box = YuuBox() + limits = ResourceLimits(memory_mb=128, timeout_seconds=10) + + code = 'print("test")' + result = box.execute(code, "python", limits=limits, no_healing=True) + + assert result.exit_code == 0 diff --git a/yuubox-core/Cargo.toml b/yuubox-core/Cargo.toml new file mode 100644 index 0000000..01b9fd6 --- /dev/null +++ b/yuubox-core/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "yuubox-core" +version = "1.0.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +name = "yuubox_core" +crate-type = ["cdylib"] + +[dependencies] +pyo3 = { version = "0.20", features = ["extension-module", "abi3-py39"] } +tokio = { version = "1.35", features = ["full"] } +bollard = "0.15" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +chrono = "0.4" +anyhow = "1.0" +thiserror = "1.0" +uuid = { version = "1.6", features = ["v4"] } +futures-util = "0.3" + +[dev-dependencies] +tokio-test = "0.4" diff --git a/yuubox-core/src/container.rs b/yuubox-core/src/container.rs new file mode 100644 index 0000000..1092b70 --- /dev/null +++ b/yuubox-core/src/container.rs @@ -0,0 +1,222 @@ +use bollard::Docker; +use bollard::container::{Config, CreateContainerOptions, RemoveContainerOptions, StartContainerOptions, WaitContainerOptions}; +use bollard::models::{HostConfig, RestartPolicy, RestartPolicyNameEnum}; +use bollard::exec::{CreateExecOptions, StartExecResults}; +use anyhow::{Result, anyhow}; +use std::collections::HashMap; +use tokio::time::{timeout, Duration}; +use futures_util::stream::StreamExt; + +use crate::limits::ResourceLimits; + +pub struct ContainerResult { + pub stdout: String, + pub stderr: String, + pub exit_code: i64, + pub memory_used: u64, + pub cpu_time: f64, +} + +pub struct ContainerManager { + docker: Docker, +} + +impl ContainerManager { + pub async fn new() -> Result { + let docker = Docker::connect_with_socket_defaults() + .map_err(|e| anyhow!("Failed to connect to Docker: {}", e))?; + + Ok(Self { docker }) + } + + pub async fn execute( + &self, + code: &str, + language: &str, + limits: &ResourceLimits, + ) -> Result { + let image = self.get_image(language)?; + let command = self.build_command(code, language)?; + + let host_config = Some(HostConfig { + memory: Some(limits.memory_bytes as i64), + nano_cpus: Some((limits.cpu_quota * 1_000_000_000.0) as i64), + network_mode: Some("none".to_string()), + read_only_root_fs: Some(true), + tmpfs: Some({ + let mut map = HashMap::new(); + map.insert("/tmp".to_string(), "size=100m".to_string()); + map + }), + cap_drop: Some(vec!["ALL".to_string()]), + restart_policy: Some(RestartPolicy { + name: Some(RestartPolicyNameEnum::NO), + ..Default::default() + }), + ..Default::default() + }); + + let config = Config { + image: Some(image.clone()), + cmd: Some(command), + working_dir: Some("/workspace".to_string()), + user: Some("1000:1000".to_string()), + host_config, + attach_stdout: Some(true), + attach_stderr: Some(true), + tty: Some(false), + ..Default::default() + }; + + let container_name = format!("yuubox-{}", uuid::Uuid::new_v4()); + + let container = self.docker + .create_container( + Some(CreateContainerOptions { + name: &container_name, + ..Default::default() + }), + config, + ) + .await?; + + let container_id = container.id.clone(); + + let exec_result = timeout( + Duration::from_secs(limits.timeout_seconds), + self.run_container(&container_id) + ).await; + + let result = match exec_result { + Ok(Ok(res)) => res, + Ok(Err(e)) => { + self.cleanup_container(&container_id).await?; + return Err(e); + } + Err(_) => { + self.docker.kill_container::(&container_id, None).await.ok(); + self.cleanup_container(&container_id).await?; + return Err(anyhow!("Execution timeout after {} seconds", limits.timeout_seconds)); + } + }; + + self.cleanup_container(&container_id).await?; + + Ok(result) + } + + async fn run_container(&self, container_id: &str) -> Result { + self.docker.start_container::(container_id, None).await?; + + let mut wait_stream = self.docker.wait_container( + container_id, + Some(WaitContainerOptions { + condition: "not-running", + }) + ); + + let mut exit_code = 0i64; + while let Some(wait_result) = wait_stream.next().await { + if let Ok(status) = wait_result { + exit_code = status.status_code; + } + } + + let logs = self.docker.logs::( + container_id, + Some(bollard::container::LogsOptions { + stdout: true, + stderr: true, + ..Default::default() + }) + ); + + let mut stdout = String::new(); + let mut stderr = String::new(); + + let log_output = logs.collect::>().await; + for log in log_output { + if let Ok(log_line) = log { + match log_line { + bollard::container::LogOutput::StdOut { message } => { + stdout.push_str(&String::from_utf8_lossy(&message)); + } + bollard::container::LogOutput::StdErr { message } => { + stderr.push_str(&String::from_utf8_lossy(&message)); + } + _ => {} + } + } + } + + Ok(ContainerResult { + stdout, + stderr, + exit_code, + memory_used: 0, + cpu_time: 0.0, + }) + } + + async fn cleanup_container(&self, container_id: &str) -> Result<()> { + self.docker.remove_container( + container_id, + Some(RemoveContainerOptions { + force: true, + ..Default::default() + }) + ).await?; + + Ok(()) + } + + pub async fn cleanup(&self) -> Result<()> { + Ok(()) + } + + fn get_image(&self, language: &str) -> Result { + let image = match language.to_lowercase().as_str() { + "python" => "python:3.11-slim", + "javascript" | "js" | "node" => "node:20-slim", + "rust" => "rust:1.75-slim", + "go" => "golang:1.21-alpine", + "java" => "openjdk:17-slim", + _ => return Err(anyhow!("Unsupported language: {}", language)), + }; + + Ok(image.to_string()) + } + + fn build_command(&self, code: &str, language: &str) -> Result> { + let command = match language.to_lowercase().as_str() { + "python" => vec![ + "python".to_string(), + "-c".to_string(), + code.to_string(), + ], + "javascript" | "js" | "node" => vec![ + "node".to_string(), + "-e".to_string(), + code.to_string(), + ], + "rust" => vec![ + "sh".to_string(), + "-c".to_string(), + format!("echo '{}' > /tmp/main.rs && rustc /tmp/main.rs -o /tmp/main && /tmp/main", code), + ], + "go" => vec![ + "sh".to_string(), + "-c".to_string(), + format!("echo '{}' > /tmp/main.go && go run /tmp/main.go", code), + ], + "java" => vec![ + "sh".to_string(), + "-c".to_string(), + format!("echo '{}' > /tmp/Main.java && javac /tmp/Main.java && java -cp /tmp Main", code), + ], + _ => return Err(anyhow!("Unsupported language: {}", language)), + }; + + Ok(command) + } +} diff --git a/yuubox-core/src/lib.rs b/yuubox-core/src/lib.rs new file mode 100644 index 0000000..db23d0a --- /dev/null +++ b/yuubox-core/src/lib.rs @@ -0,0 +1,96 @@ +use pyo3::prelude::*; +use pyo3::exceptions::PyRuntimeError; +use tokio::runtime::Runtime; +use std::time::Instant; + +mod container; +mod limits; +mod monitor; + +use container::ContainerManager; +use limits::ResourceLimits; + +#[pyclass] +#[derive(Clone)] +pub struct ExecutionResult { + #[pyo3(get)] + pub stdout: String, + #[pyo3(get)] + pub stderr: String, + #[pyo3(get)] + pub exit_code: i64, + #[pyo3(get)] + pub execution_time: f64, + #[pyo3(get)] + pub memory_used: u64, + #[pyo3(get)] + pub cpu_time: f64, +} + +#[pyclass] +pub struct ContainerExecutor { + runtime: Runtime, + manager: ContainerManager, +} + +#[pymethods] +impl ContainerExecutor { + #[new] + pub fn new() -> PyResult { + let runtime = Runtime::new() + .map_err(|e| PyRuntimeError::new_err(format!("Failed to create runtime: {}", e)))?; + + let manager = runtime.block_on(async { + ContainerManager::new().await + }).map_err(|e| PyRuntimeError::new_err(format!("Failed to create manager: {}", e)))?; + + Ok(Self { runtime, manager }) + } + + pub fn execute( + &self, + code: String, + language: String, + memory_mb: Option, + cpu_quota: Option, + timeout_seconds: Option, + ) -> PyResult { + let limits = ResourceLimits { + memory_bytes: memory_mb.unwrap_or(256) * 1024 * 1024, + cpu_quota: cpu_quota.unwrap_or(1.0), + timeout_seconds: timeout_seconds.unwrap_or(60), + }; + + let start = Instant::now(); + + let result = self.runtime.block_on(async { + self.manager.execute(&code, &language, &limits).await + }).map_err(|e| PyRuntimeError::new_err(format!("Execution failed: {}", e)))?; + + let execution_time = start.elapsed().as_secs_f64(); + + Ok(ExecutionResult { + stdout: result.stdout, + stderr: result.stderr, + exit_code: result.exit_code, + execution_time, + memory_used: result.memory_used, + cpu_time: result.cpu_time, + }) + } + + pub fn cleanup(&self) -> PyResult<()> { + self.runtime.block_on(async { + self.manager.cleanup().await + }).map_err(|e| PyRuntimeError::new_err(format!("Cleanup failed: {}", e)))?; + + Ok(()) + } +} + +#[pymodule] +fn yuubox_core(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/yuubox-core/src/limits.rs b/yuubox-core/src/limits.rs new file mode 100644 index 0000000..14061a4 --- /dev/null +++ b/yuubox-core/src/limits.rs @@ -0,0 +1,16 @@ +#[derive(Debug, Clone)] +pub struct ResourceLimits { + pub memory_bytes: u64, + pub cpu_quota: f64, + pub timeout_seconds: u64, +} + +impl Default for ResourceLimits { + fn default() -> Self { + Self { + memory_bytes: 256 * 1024 * 1024, + cpu_quota: 1.0, + timeout_seconds: 60, + } + } +} diff --git a/yuubox-core/src/monitor.rs b/yuubox-core/src/monitor.rs new file mode 100644 index 0000000..6960836 --- /dev/null +++ b/yuubox-core/src/monitor.rs @@ -0,0 +1,17 @@ +use std::time::Instant; + +pub struct ResourceMonitor { + start_time: Instant, +} + +impl ResourceMonitor { + pub fn new() -> Self { + Self { + start_time: Instant::now(), + } + } + + pub fn elapsed(&self) -> f64 { + self.start_time.elapsed().as_secs_f64() + } +} diff --git a/yuubox-core/tests/test_container.rs b/yuubox-core/tests/test_container.rs new file mode 100644 index 0000000..23e2124 --- /dev/null +++ b/yuubox-core/tests/test_container.rs @@ -0,0 +1,7 @@ +#[cfg(test)] +mod tests { + #[test] + fn test_basic() { + assert_eq!(2 + 2, 4); + } +} diff --git a/yuubox/__init__.py b/yuubox/__init__.py new file mode 100644 index 0000000..ab0d076 --- /dev/null +++ b/yuubox/__init__.py @@ -0,0 +1,15 @@ +"""YuuBox - Self-Healing Code Execution System""" + +__version__ = "1.0.0" + +from yuubox.executor import YuuBox, ExecutionResult, ResourceLimits +from yuubox.exceptions import YuuBoxError, ExecutionError + +__all__ = [ + "__version__", + "YuuBox", + "ExecutionResult", + "ResourceLimits", + "YuuBoxError", + "ExecutionError", +] diff --git a/yuubox/analyzer.py b/yuubox/analyzer.py new file mode 100644 index 0000000..33af81b --- /dev/null +++ b/yuubox/analyzer.py @@ -0,0 +1,59 @@ +import re +from typing import Dict, Any, Optional + +class ErrorAnalyzer: + """Analyzes execution errors""" + + def analyze(self, stderr: str, language: str, code: str) -> Dict[str, Any]: + if language == "python": + return self._analyze_python(stderr) + elif language in ["javascript", "js", "node"]: + return self._analyze_javascript(stderr) + elif language == "rust": + return self._analyze_rust(stderr) + else: + return self._generic_analysis(stderr) + + def _analyze_python(self, stderr: str) -> Dict[str, Any]: + lines = stderr.split("\n") + for i, line in enumerate(lines): + if "Traceback" in line: + last_line = lines[-1] if lines[-1].strip() else lines[-2] + match = re.match(r"(\w+Error): (.+)", last_line) + if match: + line_match = re.search(r'line (\d+)', stderr) + return { + "type": match.group(1), + "message": match.group(2), + "line": int(line_match.group(1)) if line_match else None, + "stack_trace": stderr, + } + return self._generic_analysis(stderr) + + def _analyze_javascript(self, stderr: str) -> Dict[str, Any]: + match = re.search(r"(\w+Error): (.+)", stderr) + if match: + return { + "type": match.group(1), + "message": match.group(2), + "stack_trace": stderr, + } + return self._generic_analysis(stderr) + + def _analyze_rust(self, stderr: str) -> Dict[str, Any]: + if "error[E" in stderr: + match = re.search(r"error\[E\d+\]: (.+)", stderr) + if match: + return { + "type": "CompilerError", + "message": match.group(1), + "stack_trace": stderr, + } + return self._generic_analysis(stderr) + + def _generic_analysis(self, stderr: str) -> Dict[str, Any]: + return { + "type": "ExecutionError", + "message": stderr[:300].strip(), + "stack_trace": stderr, + } diff --git a/yuubox/api.py b/yuubox/api.py new file mode 100644 index 0000000..8b4bc72 --- /dev/null +++ b/yuubox/api.py @@ -0,0 +1,42 @@ +from typing import Optional +from fastapi import FastAPI +from pydantic import BaseModel + +from yuubox import YuuBox, ResourceLimits + +app = FastAPI(title="YuuBox API", version="1.0.0") + +class ExecuteRequest(BaseModel): + code: str + language: str + max_iterations: Optional[int] = 5 + timeout: Optional[int] = 60 + memory_mb: Optional[int] = 256 + no_healing: Optional[bool] = False + +@app.post("/execute") +async def execute(request: ExecuteRequest): + """Execute code with self-healing""" + box = YuuBox(max_iterations=request.max_iterations) + + result = box.execute( + code=request.code, + language=request.language, + limits=ResourceLimits( + memory_mb=request.memory_mb, + timeout_seconds=request.timeout, + ), + no_healing=request.no_healing, + ) + + return { + "success": result.success, + "stdout": result.stdout, + "stderr": result.stderr, + "iterations": result.iterations, + "execution_time": result.execution_time, + } + +@app.get("/health") +async def health(): + return {"status": "healthy"} diff --git a/yuubox/cli.py b/yuubox/cli.py new file mode 100644 index 0000000..9eb4341 --- /dev/null +++ b/yuubox/cli.py @@ -0,0 +1,69 @@ +import sys +import click +from rich.console import Console +from rich.progress import Progress, SpinnerColumn, TextColumn + +from yuubox import YuuBox, ResourceLimits + +console = Console() + +@click.group() +@click.version_option("1.0.0") +def cli(): + """YuuBox - Self-Healing Code Execution""" + pass + +@cli.command() +@click.argument("file", type=click.Path(exists=True)) +@click.option("--language", "-l") +@click.option("--max-iterations", default=5) +@click.option("--no-healing", is_flag=True) +@click.option("--timeout", default=60) +@click.option("--memory", default=256) +def run(file, language, max_iterations, no_healing, timeout, memory): + """Execute code file with self-healing""" + + with open(file) as f: + code = f.read() + + if not language: + if file.endswith(".py"): + language = "python" + elif file.endswith(".js"): + language = "javascript" + elif file.endswith(".rs"): + language = "rust" + else: + console.print("[red]Cannot detect language. Use --language[/red]") + sys.exit(1) + + console.print(f"\n[bold blue]Executing {file}[/bold blue]") + console.print(f"[dim]Language: {language}, Max iterations: {max_iterations}[/dim]\n") + + box = YuuBox(max_iterations=max_iterations) + + with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}")) as progress: + task = progress.add_task("Running...", total=None) + result = box.execute( + code, language, + limits=ResourceLimits(memory_mb=memory, timeout_seconds=timeout), + no_healing=no_healing, + ) + progress.remove_task(task) + + if result.success: + console.print(f"\n[bold green]✓ Success after {result.iterations} iteration(s)[/bold green]\n") + if result.stdout: + console.print("[bold]Output:[/bold]") + console.print(result.stdout) + else: + console.print(f"\n[bold red]✗ Failed after {result.iterations} iteration(s)[/bold red]\n") + console.print("[bold]Error:[/bold]") + console.print(result.stderr[:500]) + sys.exit(1) + +def main(): + cli() + +if __name__ == "__main__": + main() diff --git a/yuubox/exceptions.py b/yuubox/exceptions.py new file mode 100644 index 0000000..be01a28 --- /dev/null +++ b/yuubox/exceptions.py @@ -0,0 +1,15 @@ +class YuuBoxError(Exception): + """Base exception""" + pass + +class ExecutionError(YuuBoxError): + """Execution failed""" + pass + +class DockerError(YuuBoxError): + """Docker error""" + pass + +class HealingError(YuuBoxError): + """Healing failed""" + pass diff --git a/yuubox/executor.py b/yuubox/executor.py new file mode 100644 index 0000000..313a554 --- /dev/null +++ b/yuubox/executor.py @@ -0,0 +1,128 @@ +import time +from dataclasses import dataclass, field +from typing import List, Optional + +try: + from yuubox.yuubox_core import ContainerExecutor as RustExecutor +except ImportError: + RustExecutor = None + +from yuubox.analyzer import ErrorAnalyzer +from yuubox.healer import YuukiHealer +from yuubox.exceptions import ExecutionError + +@dataclass +class ResourceLimits: + memory_mb: int = 256 + cpu_quota: float = 1.0 + timeout_seconds: int = 60 + +@dataclass +class ErrorReport: + error_type: str + error_message: str + iteration: int + line_number: Optional[int] = None + +@dataclass +class ExecutionResult: + success: bool + stdout: str + stderr: str + exit_code: int + iterations: int + execution_time: float + final_code: str + error_history: List[ErrorReport] = field(default_factory=list) + +class YuuBox: + """Self-healing code executor (Rust + Python hybrid)""" + + def __init__(self, max_iterations: int = 5, yuuki_api_url: Optional[str] = None): + if RustExecutor is None: + raise ImportError("Rust core not compiled. Run: maturin develop") + + self.max_iterations = max_iterations + self.rust_executor = RustExecutor() + self.analyzer = ErrorAnalyzer() + self.healer = YuukiHealer(yuuki_api_url) + + def execute( + self, + code: str, + language: str, + limits: Optional[ResourceLimits] = None, + no_healing: bool = False, + ) -> ExecutionResult: + limits = limits or ResourceLimits() + current_code = code + error_history = [] + start_time = time.time() + + for iteration in range(1, self.max_iterations + 1): + # Execute in Rust (FAST!) + result = self.rust_executor.execute( + current_code, + language, + limits.memory_mb, + limits.cpu_quota, + limits.timeout_seconds, + ) + + if result.exit_code == 0: + return ExecutionResult( + success=True, + stdout=result.stdout, + stderr=result.stderr, + exit_code=0, + iterations=iteration, + execution_time=time.time() - start_time, + final_code=current_code, + error_history=error_history, + ) + + if no_healing: + return ExecutionResult( + success=False, + stdout=result.stdout, + stderr=result.stderr, + exit_code=result.exit_code, + iterations=1, + execution_time=time.time() - start_time, + final_code=current_code, + error_history=error_history, + ) + + # Analyze error (Python) + error = self.analyzer.analyze(result.stderr, language, current_code) + error_report = ErrorReport( + error_type=error["type"], + error_message=error["message"], + iteration=iteration, + line_number=error.get("line"), + ) + error_history.append(error_report) + + # Heal with Yuuki (Python) + try: + fixed_code = self.healer.fix(current_code, error, language, error_history) + if not fixed_code or fixed_code == current_code: + break + current_code = fixed_code + except Exception as e: + break + + return ExecutionResult( + success=False, + stdout=result.stdout, + stderr=result.stderr, + exit_code=result.exit_code, + iterations=iteration, + execution_time=time.time() - start_time, + final_code=current_code, + error_history=error_history, + ) + + def cleanup(self): + """Cleanup resources""" + self.rust_executor.cleanup() diff --git a/yuubox/healer.py b/yuubox/healer.py new file mode 100644 index 0000000..2bd77f6 --- /dev/null +++ b/yuubox/healer.py @@ -0,0 +1,90 @@ +from typing import Optional, List, Dict, Any +import requests + +class YuukiHealer: + """Heals code using Yuuki AI""" + + DEFAULT_API = "https://opceanai-yuuki-api.hf.space" + + def __init__(self, api_url: Optional[str] = None): + self.api_url = (api_url or self.DEFAULT_API).rstrip("/") + self.session = requests.Session() + + def fix( + self, + code: str, + error: Dict[str, Any], + language: str, + error_history: List, + ) -> str: + prompt = self._build_prompt(code, error, language, error_history) + + try: + response = self.session.post( + f"{self.api_url}/generate", + json={ + "prompt": prompt, + "max_new_tokens": 1000, + "temperature": 0.3, + "model": "yuuki-best", + }, + timeout=30, + ) + + if not response.ok: + return code + + data = response.json() + fixed = self._extract_code(data) + return fixed if fixed else code + + except Exception: + return code + + def _build_prompt(self, code: str, error: Dict, language: str, history: List) -> str: + parts = [ + f"Fix this {language} code that has an error.", + "", + "Code:", + f"```{language}", + code, + "```", + "", + f"Error: {error['type']}", + f"Message: {error['message']}", + ] + + if len(history) > 1: + parts.append(f"\nPrevious {len(history)-1} attempts failed.") + + parts.extend([ + "", + "Provide ONLY the corrected code without explanations.", + ]) + + return "\n".join(parts) + + def _extract_code(self, data) -> str: + if isinstance(data, str): + code = data + elif isinstance(data, dict): + code = data.get("generated_text") or data.get("response") or "" + else: + return "" + + code = code.strip() + if code.startswith("```"): + lines = code.split("\n") + lines = lines[1:] + if lines and lines[-1].strip() == "```": + lines = lines[:-1] + code = "\n".join(lines) + + for cutoff in ["User:", "System:", "\nUser", "\nSystem"]: + if cutoff in code: + idx = code.index(cutoff) + if idx > 0: + code = code[:idx].strip() + break + + return code.strip()