diff --git a/crates/capsule-cli/Cargo.lock b/crates/capsule-cli/Cargo.lock index e099df7..ea7ae0c 100644 --- a/crates/capsule-cli/Cargo.lock +++ b/crates/capsule-cli/Cargo.lock @@ -322,7 +322,7 @@ dependencies = [ [[package]] name = "capsule-core" -version = "0.8.0" +version = "0.8.5" dependencies = [ "anyhow", "blake3", @@ -351,7 +351,7 @@ dependencies = [ [[package]] name = "capsule-run" -version = "0.8.0" +version = "0.8.5" dependencies = [ "capsule-core", "clap", diff --git a/crates/capsule-cli/Cargo.toml b/crates/capsule-cli/Cargo.toml index 1906790..b16a571 100644 --- a/crates/capsule-cli/Cargo.toml +++ b/crates/capsule-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "capsule-run" -version = "0.8.0" +version = "0.8.5" edition = "2024" description = "Secure WASM runtime to isolate and manage AI agent tasks" license = "Apache-2.0" @@ -16,7 +16,7 @@ path = "src/main.rs" [dependencies] clap = { version = "4.5.53", features = ["derive"] } -capsule-core = { version= "0.8.0", path = "../capsule-core" } +capsule-core = { version= "0.8.5", path = "../capsule-core" } tokio = { version = "1.48.0", features = ["rt", "rt-multi-thread", "macros"] } serde_json = "1" dotenvy = "0.15.7" diff --git a/crates/capsule-cli/npm/package.json b/crates/capsule-cli/npm/package.json index 375c041..1cf2508 100644 --- a/crates/capsule-cli/npm/package.json +++ b/crates/capsule-cli/npm/package.json @@ -1,6 +1,6 @@ { "name": "@capsule-run/cli", - "version": "0.8.0", + "version": "0.8.5", "description": "Secure WASM runtime to isolate and manage AI agent tasks", "bin": { "capsule": "./bin/capsule.js" @@ -29,9 +29,9 @@ "node": ">=18" }, "optionalDependencies": { - "@capsule-run/cli-darwin-arm64": "0.8.0", - "@capsule-run/cli-darwin-x64": "0.8.0", - "@capsule-run/cli-linux-x64": "0.8.0", - "@capsule-run/cli-win32-x64": "0.8.0" + "@capsule-run/cli-darwin-arm64": "0.8.5", + "@capsule-run/cli-darwin-x64": "0.8.5", + "@capsule-run/cli-linux-x64": "0.8.5", + "@capsule-run/cli-win32-x64": "0.8.5" } } diff --git a/crates/capsule-cli/pyproject.toml b/crates/capsule-cli/pyproject.toml index 50c688c..9a77702 100644 --- a/crates/capsule-cli/pyproject.toml +++ b/crates/capsule-cli/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "capsule-run" -version = "0.8.0" +version = "0.8.5" description = "Secure WASM runtime to isolate and manage AI agent tasks" readme = "docs/README-pypi.md" license = {text = "Apache-2.0"} diff --git a/crates/capsule-core/Cargo.lock b/crates/capsule-core/Cargo.lock index ca78e16..de0a23a 100644 --- a/crates/capsule-core/Cargo.lock +++ b/crates/capsule-core/Cargo.lock @@ -302,7 +302,7 @@ dependencies = [ [[package]] name = "capsule-core" -version = "0.8.0" +version = "0.8.5" dependencies = [ "anyhow", "blake3", diff --git a/crates/capsule-core/Cargo.toml b/crates/capsule-core/Cargo.toml index 9f5aa55..0a43c72 100644 --- a/crates/capsule-core/Cargo.toml +++ b/crates/capsule-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "capsule-core" -version = "0.8.0" +version = "0.8.5" edition = "2024" description = "Core library for Capsule - WASM runtime for AI agent isolation" license = "Apache-2.0" diff --git a/crates/capsule-core/src/wasm/compiler/javascript.rs b/crates/capsule-core/src/wasm/compiler/javascript.rs index 1c51386..c4874d7 100644 --- a/crates/capsule-core/src/wasm/compiler/javascript.rs +++ b/crates/capsule-core/src/wasm/compiler/javascript.rs @@ -114,22 +114,6 @@ impl JavascriptWasmCompiler { } } - fn find_package(package_name: &str, source_dir: &Path, sdk_path: &Path) -> PathBuf { - if let Some(project_node_modules) = Self::find_node_modules(source_dir) { - let project_path = project_node_modules.join(package_name); - if project_path.exists() { - return project_path; - } - } - - let sdk_node_modules = sdk_path.join("node_modules").join(package_name); - if sdk_node_modules.exists() { - return sdk_node_modules; - } - - source_dir.join("node_modules").join(package_name) - } - pub fn compile_wasm(&self, export: bool) -> Result { let source_dir = self.source_path.parent().ok_or_else(|| { JavascriptWasmCompilerError::FsError("Cannot determine source directory".to_string()) @@ -200,11 +184,9 @@ export {{ incomingHandler }}; let sdk_path_normalized = Self::normalize_path_for_command(&sdk_path); let output_wasm_normalized = Self::normalize_path_for_command(&self.output_wasm); - let unenv_path = Self::find_package("unenv", source_dir, &sdk_path_normalized); let os_polyfill_path = sdk_path_normalized.join("dist/polyfills/os.js"); let process_polyfill_path = sdk_path_normalized.join("dist/polyfills/process.js"); let fs_polyfill_path = sdk_path_normalized.join("dist/polyfills/fs.js"); - let mut esbuild_cmd = Self::npx_command(); esbuild_cmd .arg("esbuild") @@ -242,10 +224,10 @@ export {{ incomingHandler }}; .display() )); - esbuild_cmd.arg(format!("--alias:unenv={}", unenv_path.display())); - - let unenv_node_dir = unenv_path.join("dist/runtime/node"); - let mut stack = vec![(unenv_node_dir, String::new())]; + let bundled_unenv_dir = sdk_path_normalized.join("dist/polyfills/unenv-runtime"); + let bundled_node_dir = bundled_unenv_dir.join("node"); + esbuild_cmd.arg(format!("--alias:unenv={}", bundled_unenv_dir.display())); + let mut stack = vec![(bundled_node_dir, String::new())]; while let Some((dir, prefix)) = stack.pop() { let Ok(entries) = fs::read_dir(&dir) else { continue; @@ -378,19 +360,36 @@ export {{ incomingHandler }}; })?, ); - let output = Self::npx_command() - .arg("tsc") - .arg(&self.source_path) + let source_dir = self.source_path.parent().ok_or_else(|| { + JavascriptWasmCompilerError::FsError("Cannot determine source directory".to_string()) + })?; + + let cache_dir_normalized = Self::normalize_path_for_command(&self.cache_dir); + + let mut cmd = Self::npx_command(); + cmd.arg("tsc"); + + let tsconfig_path = source_dir.join("tsconfig.json"); + if tsconfig_path.exists() { + cmd.arg("--project") + .arg(Self::normalize_path_for_command(&tsconfig_path)); + } else { + cmd.arg(Self::normalize_path_for_command(&self.source_path)) + .arg("--module") + .arg("esnext") + .arg("--target") + .arg("esnext") + .arg("--moduleResolution") + .arg("bundler") + .arg("--esModuleInterop") + .arg("--skipLibCheck"); + } + + let output = cmd .arg("--outDir") - .arg(&self.cache_dir) - .arg("--module") - .arg("esnext") - .arg("--target") - .arg("esnext") - .arg("--moduleResolution") - .arg("bundler") - .arg("--esModuleInterop") - .arg("--skipLibCheck") + .arg(&cache_dir_normalized) + .arg("--rootDir") + .arg(Self::normalize_path_for_command(source_dir)) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output()?; diff --git a/crates/capsule-sdk/javascript/package-lock.json b/crates/capsule-sdk/javascript/package-lock.json index 8d3216f..05b6cab 100644 --- a/crates/capsule-sdk/javascript/package-lock.json +++ b/crates/capsule-sdk/javascript/package-lock.json @@ -1,12 +1,12 @@ { "name": "@capsule-run/sdk", - "version": "0.8.0", + "version": "0.8.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@capsule-run/sdk", - "version": "0.8.0", + "version": "0.8.5", "license": "Apache-2.0", "dependencies": { "@bytecodealliance/jco": "^1.0.0", diff --git a/crates/capsule-sdk/javascript/package.json b/crates/capsule-sdk/javascript/package.json index 48cc282..70b80d9 100644 --- a/crates/capsule-sdk/javascript/package.json +++ b/crates/capsule-sdk/javascript/package.json @@ -1,6 +1,6 @@ { "name": "@capsule-run/sdk", - "version": "0.8.0", + "version": "0.8.5", "description": "Capsule JavaScript SDK - run AI agent tasks in secure WASM sandboxes", "type": "module", "main": "./dist/index.js", @@ -30,7 +30,7 @@ "README.md" ], "scripts": { - "build": "tsc", + "build": "tsc && node scripts/copy-unenv-polyfills.mjs", "clean": "rm -rf dist" }, "keywords": [ diff --git a/crates/capsule-sdk/javascript/scripts/copy-unenv-polyfills.mjs b/crates/capsule-sdk/javascript/scripts/copy-unenv-polyfills.mjs new file mode 100644 index 0000000..beee162 --- /dev/null +++ b/crates/capsule-sdk/javascript/scripts/copy-unenv-polyfills.mjs @@ -0,0 +1,13 @@ +import { cpSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { createRequire } from 'module'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const require = createRequire(import.meta.url); + +const unenvDir = join(dirname(require.resolve('unenv/package.json')), 'dist/runtime'); +const outDir = join(__dirname, '../dist/polyfills/unenv-runtime'); + +cpSync(unenvDir, outDir, { recursive: true }); +console.log(`Copied unenv runtime to ${outDir}`); diff --git a/crates/capsule-sdk/javascript/src/polyfills/fs.ts b/crates/capsule-sdk/javascript/src/polyfills/fs.ts index d9d5290..436174d 100644 --- a/crates/capsule-sdk/javascript/src/polyfills/fs.ts +++ b/crates/capsule-sdk/javascript/src/polyfills/fs.ts @@ -3,6 +3,8 @@ * Provides both Node.js fs API */ +import { getCwd } from './process.js'; + declare const globalThis: { 'wasi:filesystem/types': any; 'wasi:filesystem/preopens': any; @@ -76,11 +78,24 @@ function normalizePath(path: string): string { return path; } +/** + * Absolute paths pass through unchanged. + */ +function getEffectivePath(path: string): string { + if (path.startsWith('/')) return path; + + const cwd = getCwd(); + if (cwd === '.' || cwd === '') return path; + if (path === '.') return cwd; + if (path.startsWith('./')) return cwd.replace(/\/+$/, '') + '/' + path.slice(2); + return cwd.replace(/\/+$/, '') + '/' + path; +} + function resolvePath(path: string): { dir: Descriptor; relativePath: string } | null { const preopens = getPreopenedDirs(); if (preopens.length === 0) return null; - const normalizedPath = normalizePath(path); + const normalizedPath = normalizePath(getEffectivePath(path)); let catchAll: { dir: Descriptor; relativePath: string } | null = null; @@ -298,9 +313,6 @@ export function readdir( // --------------------------------------------------------------------------- // Sync implementations -// wasi:filesystem/types@0.2.0 descriptor methods (openAt, read, write, stat, -// readDirectory, etc.) are direct component-model imports — they are -// synchronous from JS's perspective. No asyncify, no event-loop blocking. // --------------------------------------------------------------------------- function enoent(path: string): Error { @@ -312,7 +324,6 @@ function enoent(path: string): Error { /** * Read file contents synchronously. - * Backed by wasi:filesystem/types@0.2.0 — fully synchronous. */ export function readFileSync(path: string, options?: ReadFileOptions | Encoding): string | Uint8Array { const resolved = resolvePath(path); @@ -332,7 +343,6 @@ export function readFileSync(path: string, options?: ReadFileOptions | Encoding) /** * Write data to a file synchronously. - * Backed by wasi:filesystem/types@0.2.0 — fully synchronous. */ export function writeFileSync(path: string, data: string | Uint8Array, _options?: WriteFileOptions | Encoding): void { const resolved = resolvePath(path); @@ -354,7 +364,6 @@ export function writeFileSync(path: string, data: string | Uint8Array, _options? /** * Append data to a file synchronously, creating it if it doesn't exist. - * Backed by wasi:filesystem/types@0.2.0 — fully synchronous. */ export function appendFileSync(path: string, data: string | Uint8Array, _options?: WriteFileOptions | Encoding): void { const resolved = resolvePath(path); @@ -377,7 +386,6 @@ export function appendFileSync(path: string, data: string | Uint8Array, _options /** * Read directory contents synchronously. - * Backed by wasi:filesystem/types@0.2.0 — fully synchronous. */ export function readdirSync(path: string, _options?: any): string[] { const resolved = resolvePath(path); @@ -431,7 +439,6 @@ function makeStatResult(type: 'file' | 'directory' | 'notfound', size: bigint = /** * Get file stats synchronously. - * Backed by wasi:filesystem/types@0.2.0 — fully synchronous. */ export function statSync(path: string): StatResult { const resolved = resolvePath(path); @@ -456,7 +463,6 @@ export function lstatSync(path: string): StatResult { /** * Create a directory synchronously. - * Backed by wasi:filesystem/types@0.2.0 — fully synchronous. */ export function mkdirSync(path: string, options?: MkdirOptions): void { if (options?.recursive) { @@ -481,7 +487,6 @@ export function mkdirSync(path: string, options?: MkdirOptions): void { /** * Remove a directory synchronously. - * Backed by wasi:filesystem/types@0.2.0 — fully synchronous. */ export function rmdirSync(path: string, _options?: RmdirOptions): void { const resolved = resolvePath(path); @@ -495,7 +500,6 @@ export function rmdirSync(path: string, _options?: RmdirOptions): void { /** * Remove a file or directory synchronously. - * Backed by wasi:filesystem/types@0.2.0 — fully synchronous. */ export function rmSync(path: string, options?: RmOptions): void { const resolved = resolvePath(path); @@ -527,7 +531,6 @@ export function rmSync(path: string, options?: RmOptions): void { /** * Remove a file synchronously. - * Backed by wasi:filesystem/types@0.2.0 — fully synchronous. */ export function unlinkSync(path: string): void { const resolved = resolvePath(path); @@ -541,7 +544,6 @@ export function unlinkSync(path: string): void { /** * Copy a file synchronously. - * Backed by wasi:filesystem/types@0.2.0 — fully synchronous. */ export function copyFileSync(src: string, dest: string): void { const data = readFileSync(src) as Uint8Array; @@ -550,8 +552,6 @@ export function copyFileSync(src: string, dest: string): void { /** * Rename a file or directory synchronously. - * Falls back to copy+delete since wasi:filesystem/types@0.2.0 - * does not expose a rename/link-at in the current WIT binding. */ export function renameSync(oldPath: string, newPath: string): void { try { @@ -582,7 +582,6 @@ export function accessSync(path: string, _mode?: number): void { /** * Check if a path exists synchronously. - * Backed by wasi:filesystem/types@0.2.0 — fully synchronous. */ export function existsSync(path: string): boolean { const resolved = resolvePath(path); diff --git a/crates/capsule-sdk/javascript/src/polyfills/process.ts b/crates/capsule-sdk/javascript/src/polyfills/process.ts index 318ad48..01282c9 100644 --- a/crates/capsule-sdk/javascript/src/polyfills/process.ts +++ b/crates/capsule-sdk/javascript/src/polyfills/process.ts @@ -70,22 +70,44 @@ function getArgv(): string[] { } /** - * Get current working directory from WASI + * Initialized from wasi:cli/environment initialCwd(), falls back to '.'. */ -function getCwd(): string { - const bindings = getEnvBindings(); - if (!bindings || typeof bindings.initialCwd !== 'function') { - return '/'; - } - +let _virtualCwd: string = (() => { try { - const cwd = bindings.initialCwd(); - return cwd || '/'; - } catch { - return '/'; + const env = (globalThis as any)['wasi:cli/environment']; + if (env && typeof env.initialCwd === 'function') { + return env.initialCwd() ?? '.'; + } + } catch {} + return '.'; +})(); + +/** + * Internal setter for virtual CWD — handles relative and absolute paths. + */ +function setCwd(directory: string): void { + if (!directory) return; + if (directory.startsWith('/')) { + _virtualCwd = directory.replace(/\/+$/, '') || '/'; + } else if (directory === '..') { + const parts = _virtualCwd.split('/').filter(Boolean); + parts.pop(); + _virtualCwd = parts.join('/') || '.'; + } else { + _virtualCwd = (_virtualCwd === '.' || _virtualCwd === '') + ? directory.replace(/\/+$/, '') + : _virtualCwd.replace(/\/+$/, '') + '/' + directory.replace(/\/+$/, ''); } } +/** + * Returns the current virtual CWD. + * Exported so the fs polyfill can resolve relative paths against it. + */ +export function getCwd(): string { + return _virtualCwd; +} + const process = { /** * Environment variables @@ -102,17 +124,20 @@ const process = { }, /** - * Returns the current working directory + * Returns the current virtual working directory. + * Backed by in-process state, same as CPython's WASI libc. */ cwd(): string { return getCwd(); }, /** - * Change directory (not supported in WASI) + * Change the virtual working directory. + * Affects all relative path resolution in the fs polyfill. + * Does not perform any real syscall — mirrors CPython WASI behaviour. */ chdir(directory: string): void { - throw new Error('process.chdir() is not supported in WASI environment'); + setCwd(directory); }, /** diff --git a/crates/capsule-sdk/python/pyproject.toml b/crates/capsule-sdk/python/pyproject.toml index bfb15d4..e4723c9 100644 --- a/crates/capsule-sdk/python/pyproject.toml +++ b/crates/capsule-sdk/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "capsule" -version = "0.8.0" +version = "0.8.5" description = "Capsule Python SDK - run AI agent tasks in secure WASM sandboxes" readme = "README.md" requires-python = ">=3.10" diff --git a/crates/capsule-sdk/python/src/capsule/socket.py b/crates/capsule-sdk/python/src/capsule/socket.py index 431a15e..137df43 100644 --- a/crates/capsule-sdk/python/src/capsule/socket.py +++ b/crates/capsule-sdk/python/src/capsule/socket.py @@ -144,9 +144,6 @@ def _execute_http_request(self): request_str = self._send_buffer.decode('utf-8', errors='replace') method, url, headers_dict, body = self._parse_http_request(request_str) - # Remove Accept-Encoding so the Rust host fetches plain text. - # The host always returns a decoded body; keeping this header would - # return compressed bytes that the client would try to decompress again. headers_dict.pop('Accept-Encoding', None) headers_dict.pop('accept-encoding', None) @@ -278,7 +275,6 @@ class SSLContext: def __init__(self, protocol=None): self.protocol = protocol - # Attributes read/written by urllib and http.client self.check_hostname = True self.verify_mode = 2 # ssl.CERT_REQUIRED self.post_handshake_auth = False @@ -339,7 +335,6 @@ def socketpair(family=SocketShim.AF_INET, type=SocketShim.SOCK_STREAM, proto=0): def getaddrinfo(host, port, family=0, type=0, proto=0, flags=0): """Get address info (simplified for HTTP/HTTPS).""" - # Return a minimal valid response family = family or SocketShim.AF_INET type = type or SocketShim.SOCK_STREAM return [(family, type, proto, '', (host, port))]