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
12 changes: 11 additions & 1 deletion crates/bashkit-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,17 @@ need them after snapshot restore.
```python
from bashkit.langchain import create_bash_tool

tool = create_bash_tool()
tool = create_bash_tool(
mounts=[
{
"host_path": "/path/to/docs",
"vfs_path": "/docs",
"writable": False,
}
],
allowed_mount_paths=["/path/to/docs"],
readonly_filesystem=True,
)
```

### PydanticAI
Expand Down
18 changes: 18 additions & 0 deletions crates/bashkit-python/bashkit/_bashkit.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,8 @@ class Bash:
external_handler: ExternalHandler | None = None,
files: dict[str, str | Callable[[], str]] | None = None,
mounts: list[dict[str, Any]] | None = None,
allowed_mount_paths: list[str] | None = None,
readonly_filesystem: bool = False,
custom_builtins: Mapping[str, BuiltinCallback] | None = None,
network: NetworkConfig | None = None,
) -> None:
Expand Down Expand Up @@ -492,6 +494,11 @@ class Bash:
``execute()``; those re-entrant calls are rejected.
files: Dict mapping VFS paths to file contents or lazy callables.
mounts: List of real host directory mount configs.
allowed_mount_paths: Host path prefixes allowed for real filesystem
mounts. Required when mounting sensitive host locations such as
paths under a user home directory.
readonly_filesystem: Deny all filesystem mutations after configured
files and mounts are applied.
custom_builtins: Constructor-time Python callbacks exposed as
bash builtins. Each callback receives a ``BuiltinContext``
with raw ``argv`` tokens and optional pipeline ``stdin``,
Expand Down Expand Up @@ -694,6 +701,8 @@ class Bash:
external_handler: ExternalHandler | None = None,
files: dict[str, str] | None = None,
mounts: list[dict[str, Any]] | None = None,
allowed_mount_paths: list[str] | None = None,
readonly_filesystem: bool = False,
custom_builtins: Mapping[str, BuiltinCallback] | None = None,
network: NetworkConfig | None = None,
) -> Bash:
Expand Down Expand Up @@ -870,6 +879,8 @@ class BashTool:
timeout_seconds: float | None = None,
files: dict[str, str | Callable[[], str]] | None = None,
mounts: list[dict[str, Any]] | None = None,
allowed_mount_paths: list[str] | None = None,
readonly_filesystem: bool = False,
custom_builtins: Mapping[str, BuiltinCallback] | None = None,
network: NetworkConfig | None = None,
) -> None:
Expand All @@ -884,6 +895,11 @@ class BashTool:
timeout_seconds: Abort execution after this duration.
files: Dict mapping VFS paths to file contents or lazy callables.
mounts: List of real host directory mount configs.
allowed_mount_paths: Host path prefixes allowed for real filesystem
mounts. Required when mounting sensitive host locations such as
paths under a user home directory.
readonly_filesystem: Deny all filesystem mutations after configured
files and mounts are applied.
custom_builtins: Constructor-time Python callbacks exposed as
bash builtins. Each callback receives a ``BuiltinContext``
and must return a stdout string, a ``BuiltinResult``, or
Expand Down Expand Up @@ -1115,6 +1131,8 @@ class BashTool:
timeout_seconds: float | None = None,
files: dict[str, str] | None = None,
mounts: list[dict[str, Any]] | None = None,
allowed_mount_paths: list[str] | None = None,
readonly_filesystem: bool = False,
custom_builtins: Mapping[str, BuiltinCallback] | None = None,
network: NetworkConfig | None = None,
) -> BashTool:
Expand Down
28 changes: 28 additions & 0 deletions crates/bashkit-python/bashkit/langchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@

from __future__ import annotations

from collections.abc import Callable
from typing import Any

try:
from langchain_core.tools import BaseTool, ToolException
from pydantic import BaseModel, Field, PrivateAttr
Expand Down Expand Up @@ -89,6 +92,10 @@ def __init__(
max_commands: int | None = None,
max_loop_iterations: int | None = None,
timeout_seconds: float | None = None,
files: dict[str, str | Callable[[], str]] | None = None,
mounts: list[dict[str, Any]] | None = None,
allowed_mount_paths: list[str] | None = None,
readonly_filesystem: bool = False,
max_output_length: int = 100_000,
**kwargs,
):
Expand All @@ -98,6 +105,10 @@ def __init__(
max_commands=max_commands,
max_loop_iterations=max_loop_iterations,
timeout_seconds=timeout_seconds,
files=files,
mounts=mounts,
allowed_mount_paths=allowed_mount_paths,
readonly_filesystem=readonly_filesystem,
)
kwargs["name"] = bash_tool.name
kwargs["description"] = bash_tool.description()
Expand Down Expand Up @@ -198,6 +209,10 @@ def create_bash_tool(
max_commands: int | None = None,
max_loop_iterations: int | None = None,
timeout_seconds: float | None = None,
files: dict[str, str | Callable[[], str]] | None = None,
mounts: list[dict[str, Any]] | None = None,
allowed_mount_paths: list[str] | None = None,
readonly_filesystem: bool = False,
) -> BashkitTool:
"""Create a LangChain-compatible Bashkit tool.

Expand All @@ -208,6 +223,15 @@ def create_bash_tool(
max_loop_iterations: Max loop iterations
timeout_seconds: Execution timeout in seconds. When set, commands
that exceed this duration are aborted with exit code 124.
files: Static VFS file mounts keyed by sandbox path.
mounts: Real host directory mounts exposed inside the sandbox.
Mounts are read-only by default; pass ``{"writable": True}``
on a mount config to allow writes.
allowed_mount_paths: Host path prefixes allowed for real filesystem
mounts. Required when mounting sensitive host locations such as
paths under a user home directory.
readonly_filesystem: Deny all filesystem mutations after configured
files and mounts are applied.

Returns:
BashkitTool instance for use with LangChain agents
Expand All @@ -231,6 +255,10 @@ def create_bash_tool(
max_commands=max_commands,
max_loop_iterations=max_loop_iterations,
timeout_seconds=timeout_seconds,
files=files,
mounts=mounts,
allowed_mount_paths=allowed_mount_paths,
readonly_filesystem=readonly_filesystem,
)


Expand Down
48 changes: 46 additions & 2 deletions crates/bashkit-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2971,6 +2971,8 @@ pub struct PyBash {
builtin_engine: Arc<PyCallbackEngine>,
files: Vec<PyFileMount>,
real_mounts: Vec<RealMountConfig>,
allowed_mount_paths: Option<Vec<String>>,
readonly_filesystem: bool,
max_commands: Option<u64>,
max_loop_iterations: Option<u64>,
max_memory: Option<u64>,
Expand Down Expand Up @@ -3025,8 +3027,12 @@ impl PyBash {
if let Some(ref net) = self.network {
builder = net.apply(builder);
}
if let Some(ref paths) = self.allowed_mount_paths {
builder = builder.allowed_mount_paths(paths.iter().map(|p| PathBuf::from(p.as_str())));
}
let files = clone_file_mounts(py, &self.files);
let builder = apply_fs_config(builder, &files, &self.real_mounts)?;
let mut builder = apply_fs_config(builder, &files, &self.real_mounts)?;
builder = builder.readonly_filesystem(self.readonly_filesystem);
Ok(apply_custom_builtins_to_builder(
py,
builder,
Expand All @@ -3051,6 +3057,8 @@ impl PyBash {
external_handler=None,
files=None,
mounts=None,
allowed_mount_paths=None,
readonly_filesystem=false,
custom_builtins=None,
network=None,
))]
Expand All @@ -3069,6 +3077,8 @@ impl PyBash {
external_handler: Option<Py<PyAny>>,
files: Option<&Bound<'_, PyDict>>,
mounts: Option<&Bound<'_, PyList>>,
allowed_mount_paths: Option<Vec<String>>,
readonly_filesystem: bool,
custom_builtins: Option<&Bound<'_, PyDict>>,
network: Option<&Bound<'_, PyDict>>,
) -> PyResult<Self> {
Expand Down Expand Up @@ -3140,7 +3150,11 @@ impl PyBash {
if let Some(ref net) = network {
builder = net.apply(builder);
}
if let Some(ref paths) = allowed_mount_paths {
builder = builder.allowed_mount_paths(paths.iter().map(|p| PathBuf::from(p.as_str())));
}
builder = apply_fs_config(builder, &files, &real_mounts)?;
builder = builder.readonly_filesystem(readonly_filesystem);
let builtin_engine = PyCallbackEngine::new(py)?;
builder = apply_custom_builtins_to_builder(py, builder, &custom_builtins);

Expand All @@ -3164,6 +3178,8 @@ impl PyBash {
builtin_engine,
files,
real_mounts,
allowed_mount_paths,
readonly_filesystem,
max_commands,
max_loop_iterations,
max_memory,
Expand Down Expand Up @@ -3390,6 +3406,8 @@ impl PyBash {
external_handler=None,
files=None,
mounts=None,
allowed_mount_paths=None,
readonly_filesystem=false,
custom_builtins=None,
network=None,
))]
Expand All @@ -3409,6 +3427,8 @@ impl PyBash {
external_handler: Option<Py<PyAny>>,
files: Option<&Bound<'_, PyDict>>,
mounts: Option<&Bound<'_, PyList>>,
allowed_mount_paths: Option<Vec<String>>,
readonly_filesystem: bool,
custom_builtins: Option<&Bound<'_, PyDict>>,
network: Option<&Bound<'_, PyDict>>,
) -> PyResult<Self> {
Expand All @@ -3426,6 +3446,8 @@ impl PyBash {
external_handler,
files,
mounts,
allowed_mount_paths,
readonly_filesystem,
custom_builtins,
network,
)?;
Expand Down Expand Up @@ -3617,6 +3639,8 @@ pub struct BashTool {
builtin_engine: Arc<PyCallbackEngine>,
files: Vec<PyFileMount>,
real_mounts: Vec<RealMountConfig>,
allowed_mount_paths: Option<Vec<String>>,
readonly_filesystem: bool,
max_commands: Option<u64>,
max_loop_iterations: Option<u64>,
max_memory: Option<u64>,
Expand Down Expand Up @@ -3656,8 +3680,12 @@ impl BashTool {
if let Some(ref net) = self.network {
builder = net.apply(builder);
}
if let Some(ref paths) = self.allowed_mount_paths {
builder = builder.allowed_mount_paths(paths.iter().map(|p| PathBuf::from(p.as_str())));
}
let files = clone_file_mounts(py, &self.files);
let builder = apply_fs_config(builder, &files, &self.real_mounts)?;
let mut builder = apply_fs_config(builder, &files, &self.real_mounts)?;
builder = builder.readonly_filesystem(self.readonly_filesystem);
Ok(apply_custom_builtins_to_builder(
py,
builder,
Expand Down Expand Up @@ -3712,6 +3740,8 @@ impl BashTool {
timeout_seconds=None,
files=None,
mounts=None,
allowed_mount_paths=None,
readonly_filesystem=false,
custom_builtins=None,
network=None,
))]
Expand All @@ -3725,6 +3755,8 @@ impl BashTool {
timeout_seconds: Option<f64>,
files: Option<&Bound<'_, PyDict>>,
mounts: Option<&Bound<'_, PyList>>,
allowed_mount_paths: Option<Vec<String>>,
readonly_filesystem: bool,
custom_builtins: Option<&Bound<'_, PyDict>>,
network: Option<&Bound<'_, PyDict>>,
) -> PyResult<Self> {
Expand Down Expand Up @@ -3760,7 +3792,11 @@ impl BashTool {
if let Some(ref net) = network {
builder = net.apply(builder);
}
if let Some(ref paths) = allowed_mount_paths {
builder = builder.allowed_mount_paths(paths.iter().map(|p| PathBuf::from(p.as_str())));
}
builder = apply_fs_config(builder, &files, &real_mounts)?;
builder = builder.readonly_filesystem(readonly_filesystem);
let builtin_engine = PyCallbackEngine::new(py)?;
builder = apply_custom_builtins_to_builder(py, builder, &custom_builtins);

Expand All @@ -3779,6 +3815,8 @@ impl BashTool {
builtin_engine,
files,
real_mounts,
allowed_mount_paths,
readonly_filesystem,
max_commands,
max_loop_iterations,
max_memory,
Expand Down Expand Up @@ -3974,6 +4012,8 @@ impl BashTool {
timeout_seconds=None,
files=None,
mounts=None,
allowed_mount_paths=None,
readonly_filesystem=false,
custom_builtins=None,
network=None,
))]
Expand All @@ -3989,6 +4029,8 @@ impl BashTool {
timeout_seconds: Option<f64>,
files: Option<&Bound<'_, PyDict>>,
mounts: Option<&Bound<'_, PyList>>,
allowed_mount_paths: Option<Vec<String>>,
readonly_filesystem: bool,
custom_builtins: Option<&Bound<'_, PyDict>>,
network: Option<&Bound<'_, PyDict>>,
) -> PyResult<Self> {
Expand All @@ -4002,6 +4044,8 @@ impl BashTool {
timeout_seconds,
files,
mounts,
allowed_mount_paths,
readonly_filesystem,
custom_builtins,
network,
)?;
Expand Down
Loading
Loading