diff --git a/Cargo.lock b/Cargo.lock index c9ba9a5aa..02fd8348a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2811,8 +2811,8 @@ dependencies = [ [[package]] name = "monty" -version = "0.0.17" -source = "git+https://github.com/pydantic/monty?rev=9b5f478#9b5f478288e8c1c0198ec4af60049738f93652c1" +version = "0.0.18" +source = "git+https://github.com/pydantic/monty?tag=v0.0.18#45a3b2d57e6ce723fed4166fb032242ece74a663" dependencies = [ "ahash", "chrono", @@ -2822,6 +2822,7 @@ dependencies = [ "itertools 0.14.0", "jiter", "libm", + "monty-macros", "num-bigint", "num-integer", "num-traits", @@ -2837,6 +2838,16 @@ dependencies = [ "strum 0.27.2", ] +[[package]] +name = "monty-macros" +version = "0.0.18" +source = "git+https://github.com/pydantic/monty?tag=v0.0.18#45a3b2d57e6ce723fed4166fb032242ece74a663" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "napi" version = "3.9.0" diff --git a/crates/bashkit/Cargo.toml b/crates/bashkit/Cargo.toml index 0a1486700..ad117c13a 100644 --- a/crates/bashkit/Cargo.toml +++ b/crates/bashkit/Cargo.toml @@ -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 } diff --git a/crates/bashkit/src/builtins/python.rs b/crates/bashkit/src/builtins/python.rs index bd04f3c90..3c0af82f2 100644 --- a/crates/bashkit/src/builtins/python.rs +++ b/crates/bashkit/src/builtins/python.rs @@ -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; @@ -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; @@ -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, cwd: &Path, env: &HashMap, ) -> 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), _ => {} } @@ -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, @@ -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( @@ -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(), _ => { @@ -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(), _ => { @@ -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(), _ => { @@ -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(), _ => { @@ -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 { @@ -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 = entries .into_iter() @@ -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 @@ -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) @@ -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")), )), } } diff --git a/crates/bashkit/src/builtins/rg/mod.rs b/crates/bashkit/src/builtins/rg/mod.rs index 395cb7df1..03ae4c385 100644 --- a/crates/bashkit/src/builtins/rg/mod.rs +++ b/crates/bashkit/src/builtins/rg/mod.rs @@ -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()) { @@ -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; } @@ -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; } diff --git a/examples/package.json b/examples/package.json index edb5828cd..adcf029eb 100644 --- a/examples/package.json +++ b/examples/package.json @@ -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", diff --git a/examples/pnpm-lock.yaml b/examples/pnpm-lock.yaml index 74cf1197e..a715abaa4 100644 --- a/examples/pnpm-lock.yaml +++ b/examples/pnpm-lock.yaml @@ -4,13 +4,18 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + langsmith: ^0.6.0 + uuid: 14.0.0 + jsondiffpatch: '>=0.7.2' + importers: .: dependencies: '@everruns/bashkit': specifier: latest - version: 0.6.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@langchain/core@1.1.48(@opentelemetry/api@1.9.1)(openai@5.23.2(zod@3.25.76)))(zod@3.25.76) + version: 0.8.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@langchain/core@1.1.48(@opentelemetry/api@1.9.1)(openai@5.23.2(zod@3.25.76)))(zod@3.25.76) devDependencies: '@ai-sdk/openai': specifier: ^3.0.50 @@ -70,8 +75,8 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@everruns/bashkit@0.6.0': - resolution: {integrity: sha512-H/msJkXIqeeIuG68PE6rnOUy/4jOoobi0obaGVm1GNyMBuEyHoLpAQVlC1jASCUHORPmINAC1XVrfWFg13mBvg==} + '@everruns/bashkit@0.8.0': + resolution: {integrity: sha512-HrbBj7pW7wCGrzMeRJutd/oU4DIKggJ5bbI2hH5Domt/wSRNrFbUWLINxQ/wxd54Nc0yzm3Qa6ieYCytEm+sHQ==} engines: {node: '>= 18'} peerDependencies: '@langchain/core': '>=0.3' @@ -182,8 +187,8 @@ packages: json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} - langsmith@0.7.2: - resolution: {integrity: sha512-3dZUwQDJluxi2ih5eIygFODtlrQKrs3Tua0Ck3l+DAUkSGFkB1Dc0JPqkGbqiVSN+TQDElnkev/ydAOAz6jndA==} + langsmith@0.6.3: + resolution: {integrity: sha512-pXrQ4/4myQvjFFOAUmt5pWRrLEZR20gzIJD7MNdUH+5/S5nLI4ZRBo/SYKC6coaYj9pYTfQdBIzcs+3kfJ5uDA==} peerDependencies: '@opentelemetry/api': '*' '@opentelemetry/exporter-trace-otlp-proto': '*' @@ -257,13 +262,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - uuid@10.0.0: - resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} - deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). - hasBin: true - - uuid@13.0.2: - resolution: {integrity: sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==} + uuid@14.0.0: + resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} hasBin: true zod@3.25.76: @@ -313,7 +313,7 @@ snapshots: tslib: 2.8.1 optional: true - '@everruns/bashkit@0.6.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@langchain/core@1.1.48(@opentelemetry/api@1.9.1)(openai@5.23.2(zod@3.25.76)))(zod@3.25.76)': + '@everruns/bashkit@0.8.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@langchain/core@1.1.48(@opentelemetry/api@1.9.1)(openai@5.23.2(zod@3.25.76)))(zod@3.25.76)': optionalDependencies: '@langchain/core': 1.1.48(@opentelemetry/api@1.9.1)(openai@5.23.2(zod@3.25.76)) '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) @@ -327,7 +327,7 @@ snapshots: '@cfworker/json-schema': 4.1.1 '@standard-schema/spec': 1.1.0 js-tiktoken: 1.0.21 - langsmith: 0.7.2(@opentelemetry/api@1.9.1)(openai@5.23.2(zod@3.25.76)) + langsmith: 0.6.3(@opentelemetry/api@1.9.1)(openai@5.23.2(zod@3.25.76)) mustache: 4.2.0 p-queue: 6.6.2 zod: 3.25.76 @@ -341,7 +341,7 @@ snapshots: '@langchain/langgraph-checkpoint@1.0.2(@langchain/core@1.1.48(@opentelemetry/api@1.9.1)(openai@5.23.2(zod@3.25.76)))': dependencies: '@langchain/core': 1.1.48(@opentelemetry/api@1.9.1)(openai@5.23.2(zod@3.25.76)) - uuid: 10.0.0 + uuid: 14.0.0 '@langchain/langgraph-sdk@1.9.5(@langchain/core@1.1.48(@opentelemetry/api@1.9.1)(openai@5.23.2(zod@3.25.76)))': dependencies: @@ -350,7 +350,7 @@ snapshots: '@types/json-schema': 7.0.15 p-queue: 9.3.0 p-retry: 7.1.1 - uuid: 13.0.2 + uuid: 14.0.0 '@langchain/langgraph@1.3.2(@langchain/core@1.1.48(@opentelemetry/api@1.9.1)(openai@5.23.2(zod@3.25.76)))(zod@3.25.76)': dependencies: @@ -359,7 +359,7 @@ snapshots: '@langchain/langgraph-sdk': 1.9.5(@langchain/core@1.1.48(@opentelemetry/api@1.9.1)(openai@5.23.2(zod@3.25.76))) '@langchain/protocol': 0.0.15 '@standard-schema/spec': 1.1.0 - uuid: 10.0.0 + uuid: 14.0.0 zod: 3.25.76 transitivePeerDependencies: - react @@ -422,7 +422,7 @@ snapshots: json-schema@0.4.0: {} - langsmith@0.7.2(@opentelemetry/api@1.9.1)(openai@5.23.2(zod@3.25.76)): + langsmith@0.6.3(@opentelemetry/api@1.9.1)(openai@5.23.2(zod@3.25.76)): dependencies: p-queue: 6.6.2 optionalDependencies: @@ -464,8 +464,6 @@ snapshots: tslib@2.8.1: optional: true - uuid@10.0.0: {} - - uuid@13.0.2: {} + uuid@14.0.0: {} zod@3.25.76: {}