Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

- feature: add deno docker runner #62

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ libs/shinkai-tools-runner/shinkai-tools-runner-resources
apps/shinkai-tool-aave-load-requester/src/bundled-resources
launch.json
.DS_Store
libs/shinkai-tools-runner/shinkai-tools-runner-execution-storage
6 changes: 4 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/shinkai-tool-aave-state/src/index.ts

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion apps/shinkai-tool-playwright-example/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as playwright from 'npm:[email protected]';
import chromePaths from 'npm:[email protected]';
import process from 'node:process';

type Configurations = {
chromePath?: string;
Expand All @@ -13,7 +14,11 @@ export const run: Run<Configurations, Parameters, Result> = async (
configurations,
parameters,
): Promise<Result> => {
const chromePath = configurations?.chromePath || chromePaths.chrome;
const chromePath =
configurations?.chromePath ||
process.env.CHROME_BIN ||
chromePaths.chrome ||
chromePaths.chromium;
console.log('executing chrome from', chromePath);
const browser = await playwright['chromium'].launch({
executablePath: chromePath,
Expand Down
2 changes: 2 additions & 0 deletions libs/shinkai-tools-runner/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ tempfile = "3.13.0"
log = "0.4.22"
once_cell = { version = "1.20.2" }
env_logger = "0.11.5"
anyhow = { version = "1.0.93" }
chrono = { version = "0.4.38" }

[build-dependencies]
copy_to_output = "2.2.0"
Expand Down
1 change: 1 addition & 0 deletions libs/shinkai-tools-runner/src/lib.test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ async fn shinkai_tool_duckduckgo_search() {

#[tokio::test]
async fn shinkai_tool_playwright_example() {

let _ = env_logger::builder()
.filter_level(log::LevelFilter::Info)
.is_test(true)
Expand Down
54 changes: 54 additions & 0 deletions libs/shinkai-tools-runner/src/tools/container_utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use std::process::Command;

#[derive(Debug, PartialEq)]
pub enum DockerStatus {
NotInstalled,
NotRunning,
Running,
}

/// Checks if Docker is available on the system by attempting to run 'docker info' command.
/// This function verifies both that Docker is installed and that the Docker daemon is running.
///
/// # Details
///
/// The function executes `docker info` which requires:
/// - Docker CLI to be installed and in PATH
/// - Docker daemon to be running and accessible
/// - Current user to have permissions to access Docker
///
/// # Returns
///
/// * `true` - If Docker is fully operational (installed, running and accessible)
/// * `false` - If Docker is not available for any reason:
/// - Docker is not installed
/// - Docker daemon is not running
/// - User lacks permissions
/// - Other Docker configuration issues
///
/// # Example
///
/// ```
/// use shinkai_tools_runner::tools::container_utils;
///
/// let docker_available = container_utils::is_docker_available();
/// if docker_available {
/// println!("docker is available and ready to use");
/// } else {
/// println!("docker is not available - check installation and permissions");
/// }
/// ```
pub fn is_docker_available() -> DockerStatus {
let docker_check = Command::new("docker").arg("info").output();

match docker_check {
Ok(output) => {
if output.status.success() {
DockerStatus::Running
} else {
DockerStatus::NotRunning
}
}
Err(_) => DockerStatus::NotInstalled,
}
}
147 changes: 147 additions & 0 deletions libs/shinkai-tools-runner/src/tools/deno_execution_storage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
use std::{io::Write, path::PathBuf};

use nanoid::nanoid;

use super::execution_context::ExecutionContext;

#[derive(Default)]
pub struct DenoExecutionStorage {
pub context: ExecutionContext,
pub code_id: String,
pub root: PathBuf,
pub root_code: PathBuf,
pub code: PathBuf,
pub code_entrypoint: PathBuf,
pub deno_cache: PathBuf,
pub logs: PathBuf,
pub log_file: PathBuf,
pub home: PathBuf,
}

impl DenoExecutionStorage {
pub fn new(context: ExecutionContext) -> Self {
let code_id = format!("{}-{}", context.code_id, nanoid!());
let root = context.storage.clone();
let root_code = root.join("code");
let code = root_code.join(code_id.clone());
let logs = context.storage.join("logs");
let log_file = logs.join(format!(
"log_{}_{}.log",
context.context_id, context.execution_id,
));
Self {
context,
code_id: code_id.clone(),
root: root.clone(),
root_code,
code: code.clone(),
code_entrypoint: code.join("index.ts"),
deno_cache: root.join("deno-cache"),
logs: logs.clone(),
log_file,
home: root.join("home"),
}
}

pub fn init(&self, code: &str, pristine_cache: Option<bool>) -> anyhow::Result<()> {
for dir in [
&self.root,
&self.root_code,
&self.code,
&self.deno_cache,
&self.logs,
&self.home,
] {
log::info!("creating directory: {}", dir.display());
std::fs::create_dir_all(dir).map_err(|e| {
log::error!("failed to create directory {}: {}", dir.display(), e);
e
})?;
}

log::info!("creating code file: {}", self.code_entrypoint.display());
std::fs::write(&self.code_entrypoint, code).map_err(|e| {
log::error!("failed to write code to index.ts: {}", e);
e
})?;

log::info!(
"creating log file if not exists: {}",
self.log_file.display()
);
if !self.log_file.exists() {
std::fs::write(&self.log_file, "").map_err(|e| {
log::error!("failed to create log file: {}", e);
e
})?;
}

if pristine_cache.unwrap_or(false) {
std::fs::remove_dir_all(&self.deno_cache)?;
std::fs::create_dir(&self.deno_cache)?;
log::info!(
"cleared deno cache directory: {}",
self.deno_cache.display()
);
}

Ok(())
}

pub fn get_relative_code_entrypoint(&self) -> anyhow::Result<String> {
self.code_entrypoint
.strip_prefix(&self.root)
.map(|p| p.to_string_lossy().to_string())
.map_err(|e| {
log::error!("failed to get relative path: {}", e);
anyhow::anyhow!("failed to get relative path: {}", e)
})
}

pub fn get_relative_deno_cache(&self) -> anyhow::Result<String> {
self.deno_cache
.strip_prefix(&self.root)
.map(|p| p.to_string_lossy().to_string())
.map_err(|e| {
log::error!("failed to get relative path: {}", e);
anyhow::anyhow!("failed to get relative path: {}", e)
})
}

pub fn append_log(&self, log: &str) -> anyhow::Result<()> {
let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
let log_line = format!(
"{},{},{},{},{}\n",
timestamp, self.context.context_id, self.context.execution_id, self.code_id, log,
);
let mut file = std::fs::OpenOptions::new()
.append(true)
.create(true) // Create the file if it doesn't exist
.open(self.log_file.clone())
.map_err(|e| {
log::error!("failed to open log file: {}", e);
e
})?;
file.write_all(log_line.as_bytes())?;
Ok(())
}
}

// We do best effort to remove ephemereal folders
impl Drop for DenoExecutionStorage {
fn drop(&mut self) {
if let Err(e) = std::fs::remove_dir_all(&self.code) {
log::warn!(
"failed to remove code directory {}: {}",
self.code.display(),
e
);
} else {
log::info!("removed code directory: {}", self.code.display());
}
}
}

#[cfg(test)]
#[path = "deno_execution_storage.test.rs"]
mod tests;
70 changes: 70 additions & 0 deletions libs/shinkai-tools-runner/src/tools/deno_execution_storage.test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use crate::tools::{
deno_execution_storage::DenoExecutionStorage, execution_context::ExecutionContext,
};

#[tokio::test]
async fn test_execution_storage_init() {
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Info)
.is_test(true)
.try_init();

let test_dir =
std::path::PathBuf::from("./shinkai-tools-runner-execution-storage/test-execution-storage");
let storage = DenoExecutionStorage::new(ExecutionContext {
storage: test_dir.clone(),
..Default::default()
});

let test_code = "console.log('test');";
storage.init(test_code, None).unwrap();

// Verify directories were created
assert!(storage.root.exists());
assert!(storage.code.exists());
assert!(storage.deno_cache.exists());
assert!(storage.logs.exists());
assert!(storage.home.exists());

// Verify code file was written correctly
let code_contents = std::fs::read_to_string(storage.code_entrypoint.clone()).unwrap();
assert_eq!(code_contents, test_code);

// Clean up test directory
std::fs::remove_dir_all(test_dir).unwrap();
}

#[tokio::test]
async fn test_execution_storage_clean_cache() {
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Info)
.is_test(true)
.try_init();

let test_dir =
std::path::PathBuf::from("./shinkai-tools-runner-execution-storage/test-clean-cache");
let storage = DenoExecutionStorage::new(ExecutionContext {
storage: test_dir.clone(),
..Default::default()
});

// Initialize with some test code
let test_code = "console.log('test');";
storage.init(test_code, None).unwrap();

// Create a test file in the cache directory
let test_cache_file = storage.deno_cache.join("test_cache.txt");
std::fs::write(&test_cache_file, "test cache content").unwrap();
assert!(test_cache_file.exists());

// Reinitialize with pristine cache enabled
storage.init(test_code, Some(true)).unwrap();

// Verify cache directory was cleared
assert!(!test_cache_file.exists());
assert!(storage.deno_cache.exists()); // Directory should still exist but be empty
assert!(std::fs::read_dir(&storage.deno_cache).unwrap().count() == 0);

// Clean up test directory
std::fs::remove_dir_all(test_dir).unwrap();
}
Loading
Loading