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
15 changes: 13 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 crates/bashkit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ unit-prefix = "0.5"
os_display = "0.1.3"

# Embedded Python interpreter (optional)
monty = { git = "https://github.com/pydantic/monty", rev = "9b5f478", optional = true }
monty = { git = "https://github.com/pydantic/monty", tag = "v0.0.18", optional = true }

# Embedded TypeScript interpreter (optional)
zapcode-core = { version = "1.5", optional = true }
Expand Down
79 changes: 40 additions & 39 deletions crates/bashkit/src/builtins/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use async_trait::async_trait;
use chrono::{Datelike, Timelike};
use monty::{
ExcType, ExtFunctionResult, FileMode, LimitedTracker, MontyDate, MontyDateTime, MontyException,
MontyFileHandle, MontyObject, MontyRun, NameLookupResult, OsFunction, PrintWriter,
MontyFileHandle, MontyObject, MontyRun, NameLookupResult, OsFunctionCall, PrintWriter,
ResourceLimits, RunProgress, dir_stat, file_stat, symlink_stat,
};
use std::collections::HashMap;
Expand Down Expand Up @@ -508,16 +508,8 @@ async fn run_python(

loop {
match progress {
RunProgress::OsCall(os_call) => {
let result = handle_os_call(
os_call.function,
&os_call.args,
&os_call.kwargs,
&fs,
cwd,
env,
)
.await;
RunProgress::OsCall(mut os_call) => {
let result = handle_os_call(os_call.take_function_call(), &fs, cwd, env).await;
match os_call.resume(result, PrintWriter::CollectString(&mut buf)) {
Ok(next) => {
progress = next;
Expand Down Expand Up @@ -607,20 +599,29 @@ async fn run_python(
// ---------------------------------------------------------------------------

/// Dispatch a Monty OsCall to the appropriate VFS operation.
///
/// Monty's `OsFunctionCall` is a tagged enum carrying typed args. We project it
/// to the generic `(positional, keyword)` `MontyObject` view via the public
/// `to_args()` and dispatch on the stable `name()` string (kept byte-identical
/// across monty releases for snapshot compatibility), keeping the per-op VFS
/// logic unchanged.
async fn handle_os_call(
function: OsFunction,
args: &[MontyObject],
kwargs: &[(MontyObject, MontyObject)],
function: OsFunctionCall,
fs: &Arc<dyn FileSystem>,
cwd: &Path,
env: &HashMap<String, String>,
) -> ExtFunctionResult {
let name = function.name();
let (args, kwargs) = function.to_args();
let args = args.as_slice();
let kwargs = kwargs.as_slice();

// Non-filesystem operations: env access, date/time
match function {
OsFunction::Getenv => return handle_getenv(args, env),
OsFunction::GetEnviron => return handle_get_environ(env),
OsFunction::DateToday => return handle_date_today(),
OsFunction::DateTimeNow => return handle_datetime_now(args),
match name {
"os.getenv" => return handle_getenv(args, env),
"os.environ" => return handle_get_environ(env),
"date.today" => return handle_date_today(),
"datetime.now" => return handle_datetime_now(args),
_ => {}
}

Expand All @@ -635,8 +636,8 @@ async fn handle_os_call(
}
};

match function {
OsFunction::Open => {
match name {
"Open" => {
let mode = match parse_open_mode(args) {
Ok(mode) => mode,
Err(err) => return err,
Expand All @@ -650,23 +651,23 @@ async fn handle_os_call(
Err(e) => map_vfs_error(e, &path),
}
}
OsFunction::Exists => {
"Path.exists" => {
let exists = fs.exists(&path).await.unwrap_or(false);
ExtFunctionResult::Return(MontyObject::Bool(exists))
}
OsFunction::IsFile => match fs.stat(&path).await {
"Path.is_file" => match fs.stat(&path).await {
Ok(meta) => ExtFunctionResult::Return(MontyObject::Bool(meta.file_type.is_file())),
Err(_) => ExtFunctionResult::Return(MontyObject::Bool(false)),
},
OsFunction::IsDir => match fs.stat(&path).await {
"Path.is_dir" => match fs.stat(&path).await {
Ok(meta) => ExtFunctionResult::Return(MontyObject::Bool(meta.file_type.is_dir())),
Err(_) => ExtFunctionResult::Return(MontyObject::Bool(false)),
},
OsFunction::IsSymlink => match fs.stat(&path).await {
"Path.is_symlink" => match fs.stat(&path).await {
Ok(meta) => ExtFunctionResult::Return(MontyObject::Bool(meta.file_type.is_symlink())),
Err(_) => ExtFunctionResult::Return(MontyObject::Bool(false)),
},
OsFunction::ReadText => match fs.read_file(&path).await {
"Path.read_text" => match fs.read_file(&path).await {
Ok(bytes) => match String::from_utf8(bytes) {
Ok(s) => ExtFunctionResult::Return(MontyObject::String(s)),
Err(_) => ExtFunctionResult::Error(MontyException::new(
Expand All @@ -679,11 +680,11 @@ async fn handle_os_call(
},
Err(e) => map_vfs_error(e, &path),
},
OsFunction::ReadBytes => match fs.read_file(&path).await {
"Path.read_bytes" => match fs.read_file(&path).await {
Ok(bytes) => ExtFunctionResult::Return(MontyObject::Bytes(bytes)),
Err(e) => map_vfs_error(e, &path),
},
OsFunction::WriteText => {
"Path.write_text" => {
let content = match args.get(1) {
Some(MontyObject::String(s)) => s.as_bytes().to_vec(),
_ => {
Expand All @@ -702,7 +703,7 @@ async fn handle_os_call(
Err(e) => map_vfs_error(e, &path),
}
}
OsFunction::WriteBytes => {
"Path.write_bytes" => {
let content = match args.get(1) {
Some(MontyObject::Bytes(b)) => b.clone(),
_ => {
Expand All @@ -718,7 +719,7 @@ async fn handle_os_call(
Err(e) => map_vfs_error(e, &path),
}
}
OsFunction::AppendText => {
"Path.append_text" => {
let content = match args.get(1) {
Some(MontyObject::String(s)) => s.as_bytes().to_vec(),
_ => {
Expand All @@ -737,7 +738,7 @@ async fn handle_os_call(
Err(e) => map_vfs_error(e, &path),
}
}
OsFunction::AppendBytes => {
"Path.append_bytes" => {
let content = match args.get(1) {
Some(MontyObject::Bytes(b)) => b.clone(),
_ => {
Expand All @@ -753,7 +754,7 @@ async fn handle_os_call(
Err(e) => map_vfs_error(e, &path),
}
}
OsFunction::Mkdir => {
"Path.mkdir" => {
let parents = get_bool_kwarg(kwargs, "parents").unwrap_or(false);
let exist_ok = get_bool_kwarg(kwargs, "exist_ok").unwrap_or(false);
match fs.mkdir(&path, parents).await {
Expand All @@ -768,15 +769,15 @@ async fn handle_os_call(
}
}
}
OsFunction::Unlink => match fs.remove(&path, false).await {
"Path.unlink" => match fs.remove(&path, false).await {
Ok(()) => ExtFunctionResult::Return(MontyObject::None),
Err(e) => map_vfs_error(e, &path),
},
OsFunction::Rmdir => match fs.remove(&path, false).await {
"Path.rmdir" => match fs.remove(&path, false).await {
Ok(()) => ExtFunctionResult::Return(MontyObject::None),
Err(e) => map_vfs_error(e, &path),
},
OsFunction::Iterdir => match fs.read_dir(&path).await {
"Path.iterdir" => match fs.read_dir(&path).await {
Ok(entries) => {
let items: Vec<MontyObject> = entries
.into_iter()
Expand All @@ -789,7 +790,7 @@ async fn handle_os_call(
}
Err(e) => map_vfs_error(e, &path),
},
OsFunction::Stat => match fs.stat(&path).await {
"Path.stat" => match fs.stat(&path).await {
Ok(meta) => {
let mtime = meta
.modified
Expand All @@ -805,7 +806,7 @@ async fn handle_os_call(
}
Err(e) => map_vfs_error(e, &path),
},
OsFunction::Rename => {
"Path.rename" => {
let target = match args.get(1) {
Some(MontyObject::Path(p)) | Some(MontyObject::String(p)) => {
resolve_python_path(p, cwd)
Expand All @@ -824,14 +825,14 @@ async fn handle_os_call(
Err(e) => map_vfs_error(e, &path),
}
}
OsFunction::Resolve | OsFunction::Absolute => {
"Path.resolve" | "Path.absolute" => {
// No symlink resolution in Bashkit VFS; just return absolute path
ExtFunctionResult::Return(MontyObject::Path(path.to_string_lossy().to_string()))
}
// Getenv/GetEnviron handled above
_ => ExtFunctionResult::Error(MontyException::new(
ExcType::OSError,
Some(format!("{function} not supported in virtual mode")),
Some(format!("{name} not supported in virtual mode")),
)),
}
}
Expand Down
43 changes: 43 additions & 0 deletions crates/bashkit/src/builtins/rg/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11548,6 +11548,40 @@ mod tests {
);
}

/// CI pins ripgrep to this version (see `RG_VERSION` in
/// `.github/workflows/ci.yml` and `scripts/install-ripgrep-ci.sh`). The
/// differential tests compare byte-for-byte against real ripgrep, whose
/// output, accepted `--colors` specs, and built-in file types vary across
/// releases, so they only run against the pinned version.
const PINNED_RG_VERSION: &str = "15.1.0";

fn real_rg_matches_pinned_version() -> bool {
let Ok(output) = std::process::Command::new("rg").arg("--version").output() else {
return false;
};
if !output.status.success() {
return false;
}
String::from_utf8_lossy(&output.stdout)
.lines()
.next()
.is_some_and(|line| line.contains(&format!("ripgrep {PINNED_RG_VERSION}")))
}

/// Returns `true` (and prints a skip notice) when the local `rg` is not the
/// pinned version, so differential tests can early-return instead of
/// emitting confusing byte-mismatch failures against an unexpected release.
fn skip_if_rg_version_mismatch(test: &str) -> bool {
if real_rg_matches_pinned_version() {
return false;
}
eprintln!(
"skipping {test}: differential tests require pinned ripgrep \
{PINNED_RG_VERSION} (install via scripts/install-ripgrep-ci.sh)"
);
true
}

fn normalize_real_rg_temp_paths(output: &[u8], tempdir: &tempfile::TempDir) -> String {
let mut stdout = String::from_utf8_lossy(output).into_owned();
if let Ok(canonical) = std::fs::canonicalize(tempdir.path()) {
Expand Down Expand Up @@ -13419,11 +13453,17 @@ mod tests {

#[test]
fn real_rg_binary_is_available_for_differential_tests() {
if skip_if_rg_version_mismatch("real_rg_binary_is_available_for_differential_tests") {
return;
}
require_real_rg();
}

#[tokio::test]
async fn diff_rg_matches_real_rg_cases() {
if skip_if_rg_version_mismatch("diff_rg_matches_real_rg_cases") {
return;
}
for case in RG_DIFF_CASES {
assert_rg_diff_case(case).await;
}
Expand All @@ -13438,6 +13478,9 @@ mod tests {
#[cfg(unix)]
#[tokio::test]
async fn diff_rg_matches_real_rg_symlink_cases() {
if skip_if_rg_version_mismatch("diff_rg_matches_real_rg_symlink_cases") {
return;
}
for case in RG_SYMLINK_DIFF_CASES {
assert_rg_symlink_diff_case(case).await;
}
Expand Down
10 changes: 6 additions & 4 deletions examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
"openai": "^5",
"zod": "^3"
},
"overrides": {
"langsmith": "^0.6.0",
"uuid": "14.0.0",
"jsondiffpatch": ">=0.7.2"
"pnpm": {
"overrides": {
"langsmith": "^0.6.0",
"uuid": "14.0.0",
"jsondiffpatch": ">=0.7.2"
}
},
"scripts": {
"examples": "node bash_basics.mjs && node data_pipeline.mjs && node llm_tool.mjs && node k8s_orchestrator.mjs && node langchain_integration.mjs && node custom_builtins.mjs",
Expand Down
Loading
Loading