Skip to content
Merged
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
4 changes: 2 additions & 2 deletions crates/capsule-cli/Cargo.lock

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

4 changes: 2 additions & 2 deletions crates/capsule-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "capsule-run"
version = "0.8.5"
version = "0.8.7"
edition = "2024"
description = "Secure WASM runtime to isolate and manage AI agent tasks"
license = "Apache-2.0"
Expand All @@ -16,7 +16,7 @@ path = "src/main.rs"

[dependencies]
clap = { version = "4.5.53", features = ["derive"] }
capsule-core = { version= "0.8.5", path = "../capsule-core" }
capsule-core = { version= "0.8.7", path = "../capsule-core" }
tokio = { version = "1.48.0", features = ["rt", "rt-multi-thread", "macros"] }
serde_json = "1"
dotenvy = "0.15.7"
10 changes: 5 additions & 5 deletions crates/capsule-cli/npm/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@capsule-run/cli",
"version": "0.8.5",
"version": "0.8.7",
"description": "Secure WASM runtime to isolate and manage AI agent tasks",
"bin": {
"capsule": "./bin/capsule.js"
Expand Down Expand Up @@ -29,9 +29,9 @@
"node": ">=18"
},
"optionalDependencies": {
"@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"
"@capsule-run/cli-darwin-arm64": "0.8.7",
"@capsule-run/cli-darwin-x64": "0.8.7",
"@capsule-run/cli-linux-x64": "0.8.7",
"@capsule-run/cli-win32-x64": "0.8.7"
}
}
2 changes: 1 addition & 1 deletion crates/capsule-cli/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "maturin"

[project]
name = "capsule-run"
version = "0.8.5"
version = "0.8.7"
description = "Secure WASM runtime to isolate and manage AI agent tasks"
readme = "docs/README-pypi.md"
license = {text = "Apache-2.0"}
Expand Down
6 changes: 6 additions & 0 deletions crates/capsule-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ pub enum Commands {
#[arg(long, value_name = "HOST[::GUEST][:ro|:rw]")]
mount: Vec<String>,

#[arg(long, value_name = "FILE", conflicts_with = "args")]
args_file: Option<String>,

#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Expand All @@ -44,6 +47,9 @@ pub enum Commands {
#[arg(long, value_name = "HOST[::GUEST][:ro|:rw]")]
mount: Vec<String>,

#[arg(long, value_name = "FILE", conflicts_with = "args")]
args_file: Option<String>,

#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Expand Down
12 changes: 7 additions & 5 deletions crates/capsule-cli/src/commands/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,13 @@ pub async fn execute(

reporter.start_progress("Initializing runtime");

let project_root = file_path
.canonicalize()
.ok()
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
let project_root = std::env::current_dir().unwrap_or_else(|_| {
file_path
.canonicalize()
.ok()
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
.unwrap_or_default()
});

load_env_variables(&project_root).map_err(RunError::IoError)?;

Expand Down
8 changes: 8 additions & 0 deletions crates/capsule-cli/src/commands/shared.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
use std::path::Path;

pub fn load_args_file(path: &str) -> Result<Vec<String>, String> {
let content = std::fs::read_to_string(path)
.map_err(|e| format!("Failed to read --args-file '{}': {}", path, e))?;
let args: Vec<String> = serde_json::from_str(&content)
.map_err(|e| format!("Failed to parse --args-file '{}': {}", path, e))?;
Ok(args)
}

pub fn load_env_variables(project_root: &Path) -> Result<(), String> {
let env_files = [".env", ".env.local", ".env.development", ".env.production"];

Expand Down
11 changes: 11 additions & 0 deletions crates/capsule-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::fmt;
use std::path::Path;

use cli::{Cli, Commands};
use commands::shared::load_args_file;
use commands::{BuildError, ExecError, RunError, build, exec, run};

#[derive(Debug)]
Expand Down Expand Up @@ -53,8 +54,13 @@ async fn main() -> Result<(), CliError> {
json,
verbose,
mount,
args_file,
args,
} => {
let args = match args_file {
Some(ref path) => load_args_file(path).map_err(CliError::RunError)?,
None => args,
};
let file_path = file.as_deref().map(Path::new);
let result = run::execute(file_path, args, mount, json, verbose).await?;

Expand All @@ -75,8 +81,13 @@ async fn main() -> Result<(), CliError> {
json,
verbose,
mount,
args_file,
args,
} => {
let args = match args_file {
Some(ref path) => load_args_file(path).map_err(CliError::ExecError)?,
None => args,
};
let result = exec::execute(Path::new(&file), args, mount, json, verbose).await?;

if json {
Expand Down
2 changes: 1 addition & 1 deletion crates/capsule-core/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 crates/capsule-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "capsule-core"
version = "0.8.5"
version = "0.8.7"
edition = "2024"
description = "Core library for Capsule - WASM runtime for AI agent isolation"
license = "Apache-2.0"
Expand Down
25 changes: 23 additions & 2 deletions crates/capsule-core/src/wasm/utilities/path_validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ fn parse_path_spec(path_spec: &str) -> (String, Option<String>, FileAccessMode)
}
}

fn to_absolute_guest_path(path: &str) -> String {
let stripped = path.trim_start_matches("./");

if stripped.starts_with('/') {
stripped.to_string()
} else {
format!("/{}", stripped)
}
}

pub fn validate_path(
path_spec: &str,
project_root: &Path,
Expand All @@ -94,7 +104,8 @@ pub fn validate_path(
return Err(PathValidationError::EscapesProjectDirectory(host_str));
}

let guest_path = guest_alias.unwrap_or_else(|| host_str.clone());
let raw_guest = guest_alias.unwrap_or_else(|| host_str.clone());
let guest_path = to_absolute_guest_path(&raw_guest);

Ok(ParsedPath {
path: resolved,
Expand Down Expand Up @@ -132,7 +143,7 @@ mod tests {

assert!(result.is_ok());
let parsed = result.unwrap();
assert_eq!(parsed.guest_path, "./.capsule_test");
assert_eq!(parsed.guest_path, "/.capsule_test");
}

#[test]
Expand Down Expand Up @@ -198,4 +209,14 @@ mod tests {
assert_eq!(guest, Some("workspace".to_string()));
assert_eq!(mode, FileAccessMode::ReadOnly);
}

#[test]
fn test_guest_path_normalization() {
assert_eq!(to_absolute_guest_path("./data"), "/data");
assert_eq!(to_absolute_guest_path("data"), "/data");
assert_eq!(to_absolute_guest_path("/data"), "/data");
assert_eq!(to_absolute_guest_path("/"), "/");
assert_eq!(to_absolute_guest_path("workspace"), "/workspace");
assert_eq!(to_absolute_guest_path("./nested/dir"), "/nested/dir");
}
}
4 changes: 2 additions & 2 deletions crates/capsule-sdk/javascript/package-lock.json

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

2 changes: 1 addition & 1 deletion crates/capsule-sdk/javascript/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@capsule-run/sdk",
"version": "0.8.5",
"version": "0.8.7",
"description": "Capsule JavaScript SDK - run AI agent tasks in secure WASM sandboxes",
"type": "module",
"main": "./dist/index.js",
Expand Down
8 changes: 5 additions & 3 deletions crates/capsule-sdk/javascript/src/polyfills/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,11 @@ function resolvePath(path: string): { dir: Descriptor; relativePath: string } |
continue;
}

if (normalizedPath.startsWith(normalizedGuest + '/')) {
const relativePath = normalizedPath.slice(normalizedGuest.length + 1);
return { dir: descriptor, relativePath };
const guestPrefix = normalizedGuest.endsWith('/') ? normalizedGuest : normalizedGuest + '/';

if (normalizedPath.startsWith(guestPrefix)) {
const relativePath = normalizedPath.slice(guestPrefix.length);
return { dir: descriptor, relativePath: relativePath || '.' };
}

if (normalizedPath === normalizedGuest) {
Expand Down
18 changes: 10 additions & 8 deletions crates/capsule-sdk/javascript/src/polyfills/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,20 +70,23 @@ function getArgv(): string[] {
}

/**
* Initialized from wasi:cli/environment initialCwd(), falls back to '.'.
* Initialized from wasi:cli/environment initialCwd(), falls back to '/'.
*/
let _virtualCwd: string = (() => {
try {
const env = (globalThis as any)['wasi:cli/environment'];
if (env && typeof env.initialCwd === 'function') {
return env.initialCwd() ?? '.';
const initial = env.initialCwd();
if (initial) {
return initial.startsWith('/') ? initial : '/' + initial;
}
}
} catch {}
return '.';
return '/';
})();

/**
* Internal setter for virtual CWD — handles relative and absolute paths.
* Internal setter for virtual CWD — handles relative and absolute paths safely via absolute resolution.
*/
function setCwd(directory: string): void {
if (!directory) return;
Expand All @@ -92,11 +95,10 @@ function setCwd(directory: string): void {
} else if (directory === '..') {
const parts = _virtualCwd.split('/').filter(Boolean);
parts.pop();
_virtualCwd = parts.join('/') || '.';
_virtualCwd = '/' + parts.join('/');
} else {
_virtualCwd = (_virtualCwd === '.' || _virtualCwd === '')
? directory.replace(/\/+$/, '')
: _virtualCwd.replace(/\/+$/, '') + '/' + directory.replace(/\/+$/, '');
const base = _virtualCwd.endsWith('/') ? _virtualCwd : _virtualCwd + '/';
_virtualCwd = (base + directory).replace(/\/+$/, '');
}
}

Expand Down
52 changes: 41 additions & 11 deletions crates/capsule-sdk/javascript/src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
*/

import { execFile } from 'child_process';
import { resolve, extname } from 'path';
import { existsSync } from 'fs';
import { resolve, extname, join } from 'path';
import { existsSync, writeFileSync, unlinkSync } from 'fs';
import { tmpdir } from 'os';
import { randomUUID } from 'crypto';
import { HostRequest } from './task';

export interface RunnerOptions {
Expand All @@ -33,6 +35,7 @@ export interface RunnerResult {
}

const WASM_EXTENSIONS = new Set(['.wasm']);
const ARGS_FILE_THRESHOLD = 8 * 1024;

/**
* Get the appropriate capsule command for the current platform
Expand All @@ -44,6 +47,16 @@ function getCapsuleCommand(capsulePath: string): string {
return capsulePath;
}

/**
* Write args to a temp file and return its path.
* Caller is responsible for deleting it.
*/
function writeArgsFile(args: string[]): string {
const path = join(tmpdir(), `capsule-args-${randomUUID()}.json`);
writeFileSync(path, JSON.stringify(args), 'utf-8');
return path;
}

/**
* Run a Capsule task from a third-party application
*
Expand All @@ -66,20 +79,37 @@ export function run(options: RunnerOptions): Promise<RunnerResult> {
}

const subcommand = isWasm ? 'exec' : 'run';
const mountFlags = mounts.flatMap(m => ['--mount', m]);

return new Promise((resolve, reject) => {
const mountFlags = mounts.flatMap(m => ['--mount', m]);
const cmdArgs = [subcommand, resolvedFile, '--json', ...mountFlags, ...args];
const serializedArgs = JSON.stringify(args);
const useArgsFile = Buffer.byteLength(serializedArgs, 'utf-8') > ARGS_FILE_THRESHOLD;

let argsFilePath: string | null = null;
let argsFlags: string[];

let executable = command;
let executionArgs = cmdArgs;
if (useArgsFile) {
argsFilePath = writeArgsFile(args);
argsFlags = ['--args-file', argsFilePath];
} else {
argsFlags = args;
}

if (process.platform === 'win32') {
executable = process.env.comspec || 'cmd.exe';
executionArgs = ['/d', '/s', '/c', command, ...cmdArgs];
}
const cmdArgs = [subcommand, resolvedFile, '--json', ...mountFlags, ...argsFlags];

let executable = command;
let executionArgs = cmdArgs;

if (process.platform === 'win32') {
executable = process.env.comspec || 'cmd.exe';
executionArgs = ['/d', '/s', '/c', command, ...cmdArgs];
}

return new Promise((resolve, reject) => {
execFile(executable, executionArgs, { cwd, encoding: 'utf-8' }, (error, stdout, stderr) => {
if (argsFilePath) {
try { unlinkSync(argsFilePath); } catch { }
}

if (error && !stdout) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
reject(new Error(
Expand Down
2 changes: 1 addition & 1 deletion crates/capsule-sdk/python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "capsule"
version = "0.8.5"
version = "0.8.7"
description = "Capsule Python SDK - run AI agent tasks in secure WASM sandboxes"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
Loading
Loading