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
6 changes: 3 additions & 3 deletions contracts/capability-parity.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@
},
"python": {
"label": "Python",
"supported": ["execution_limits", "cancellation", "deadlines", "output_caps", "cwd", "env", "runtime_env", "custom_builtins", "tool_callbacks", "network_policy", "virtual_filesystem", "mounts", "snapshots", "runtime_python", "runtime_sqlite", "interactive_terminal"],
"supported": ["execution_limits", "cancellation", "deadlines", "output_caps", "cwd", "env", "runtime_env", "custom_builtins", "tool_callbacks", "network_policy", "virtual_filesystem", "mounts", "snapshots", "runtime_python", "runtime_typescript", "runtime_sqlite", "interactive_terminal"],
"evidence": {
"execution_limits": ["crates/bashkit-python/tests/_bashkit_categories.py#test_max_commands_limits_execution"],
"cancellation": ["crates/bashkit-python/tests/_bashkit_categories.py#test_bash_cancel_after_reset_still_works"],
Expand All @@ -153,14 +153,14 @@
"mounts": ["crates/bashkit-python/tests/_bashkit_categories.py#test_bash_live_mount_preserves_state_and_unmounts"],
"snapshots": ["crates/bashkit-python/tests/_bashkit_categories.py#test_bash_snapshot_roundtrip_from_snapshot_preserves_state_and_kwargs"],
"runtime_python": ["crates/bashkit-python/tests/_bashkit_categories.py#test_bash_python_enabled"],
"runtime_typescript": ["crates/bashkit-python/tests/test_tool_runtimes.py#test_runtimes_share_virtual_filesystem"],
"runtime_sqlite": ["crates/bashkit-python/tests/test_sqlite.py#test_sqlite"],
"interactive_terminal": ["crates/bashkit-python/tests/test_terminal.py#test_call_edits_file_in_vi"]
},
"unsupported": {
"stdin": "execute() accepts a command string and optional output callback, not top-level host stdin.",
"host_call_suspension": "Python custom builtins await callbacks directly; no request and resume handle is exposed.",
"transport_hooks": "Python exposes network policy and credentials but no custom HttpTransport implementation hook.",
"runtime_typescript": "The Python package does not compile or expose the TypeScript runtime."
"transport_hooks": "Python exposes network policy and credentials but no custom HttpTransport implementation hook."
}
},
"napi": {
Expand Down
4 changes: 2 additions & 2 deletions crates/bashkit-python/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ num-bigint = "^0.4.6"
# none of which compile on wasm32-unknown-emscripten — see the wasm block
# below and knowledge/runtimes/emscripten-wheels.md.
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
bashkit = { path = "../bashkit", features = ["python", "realfs", "jq", "interop", "http_client", "sqlite", "terminal"] }
bashkit = { path = "../bashkit", features = ["python", "typescript", "realfs", "jq", "interop", "http_client", "sqlite", "terminal"] }
bashkit-scripted-tool = { path = "../bashkit-scripted-tool", features = ["python", "jq"] }
tokio = { workspace = true, features = ["rt-multi-thread"] }
# pyo3-async-runtimes hard-pulls tokio `rt-multi-thread` + `net` (mio), which
Expand All @@ -50,6 +50,6 @@ pyo3-async-runtimes = { workspace = true }
# wasm-safe base features (sync, macros, io-util, rt, time) — current-thread
# runtime only. See knowledge/runtimes/emscripten-wheels.md.
[target.'cfg(target_arch = "wasm32")'.dependencies]
bashkit = { path = "../bashkit", features = ["python", "jq", "terminal"] }
bashkit = { path = "../bashkit", features = ["python", "typescript", "jq", "terminal"] }
bashkit-scripted-tool = { path = "../bashkit-scripted-tool", features = ["python", "jq"] }
tokio = { workspace = true }
22 changes: 22 additions & 0 deletions crates/bashkit-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,28 @@ bash = Bash(
)
```

### Embedded Python and TypeScript

Native wheels include both runtimes, disabled by default. Enable only those
your application needs:

```python
from bashkit import BashTool

# Python also requires host opt-in; scripts cannot set this permission.
tool = BashTool(
python=True,
typescript=True,
env={"BASHKIT_ALLOW_INPROCESS_PYTHON": "1"},
)
assert tool.execute_sync("python -c 'print(42)'").stdout == "42\n"
assert tool.execute_sync("ts -c '42'").stdout == "42\n"
```

LangChain's `BashkitTool` and deepagents' middleware/backend accept the same
flags. Runtime flags survive `reset()`; supply them explicitly when restoring
with `BashTool.from_snapshot()` or `BashTool.from_snapshot_keyed()`.

### Live Output

```python
Expand Down
21 changes: 21 additions & 0 deletions crates/bashkit-python/bashkit/_bashkit.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,8 @@ class BashTool:
custom_builtins: Mapping[str, BuiltinCallback] | None = None,
network: NetworkConfig | None = None,
profile: ExecutionProfile = ExecutionProfile.Standard,
python: bool = False,
typescript: bool = False,
) -> None:
"""Create a new BashTool.

Expand Down Expand Up @@ -1145,6 +1147,14 @@ class BashTool:
network: Optional outbound HTTP / network configuration. See
``Bash.__init__`` for accepted keys. Preserved across
``reset()`` and ``from_snapshot()``.
profile: Execution profile (Standard or Compatibility).
python: Register sandboxed Monty Python (default ``False``).
typescript: Register sandboxed ZapCode TypeScript (default ``False``).

Embedded runtimes share the shell's virtual filesystem and stay enabled
after ``reset()``. Python additionally requires the host to provide
``env={"BASHKIT_ALLOW_INPROCESS_PYTHON": "1"}`` at construction.
An agent script cannot grant this permission with ``export``.

Example::

Expand All @@ -1155,6 +1165,13 @@ class BashTool:
... )
>>> print(tool.name)
bash
>>> tool = BashTool(
... python=True,
... typescript=True,
... env={"BASHKIT_ALLOW_INPROCESS_PYTHON": "1"},
... )
>>> tool.execute_sync("python -c 'print(21)' && ts -c '21'").stdout
'21\\n21\\n'
"""
...

Expand Down Expand Up @@ -1412,6 +1429,8 @@ class BashTool:
custom_builtins: Mapping[str, BuiltinCallback] | None = None,
network: NetworkConfig | None = None,
profile: ExecutionProfile = ExecutionProfile.Standard,
python: bool = False,
typescript: bool = False,
) -> BashTool:
"""Create a new ``BashTool`` from snapshot bytes and optional constructor kwargs."""
...
Expand All @@ -1435,6 +1454,8 @@ class BashTool:
custom_builtins: Mapping[str, BuiltinCallback] | None = None,
network: NetworkConfig | None = None,
profile: ExecutionProfile = ExecutionProfile.Standard,
python: bool = False,
typescript: bool = False,
) -> BashTool:
"""Create a new ``BashTool`` from HMAC-protected snapshot bytes."""
...
Expand Down
16 changes: 16 additions & 0 deletions crates/bashkit-python/bashkit/deepagents.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ def __init__(
max_commands: int | None = None,
max_loop_iterations: int | None = None,
timeout_seconds: float | None = None,
python: bool = False,
typescript: bool = False,
env: dict[str, str] | None = None,
):
"""Initialize middleware.

Expand All @@ -169,6 +172,10 @@ def __init__(
max_commands: Max commands (ignored if bash_tool provided)
max_loop_iterations: Max iterations (ignored if bash_tool provided)
timeout_seconds: Execution timeout in seconds (ignored if bash_tool provided)
python: Enable Monty Python (ignored if bash_tool provided)
typescript: Enable ZapCode TypeScript (ignored if bash_tool provided)
env: Initial environment for a new tool (ignored if bash_tool provided).
Python requires BASHKIT_ALLOW_INPROCESS_PYTHON=1 at construction.
"""
if bash_tool is not None:
self._bash = bash_tool
Expand All @@ -180,6 +187,9 @@ def __init__(
max_commands=max_commands,
max_loop_iterations=max_loop_iterations,
timeout_seconds=timeout_seconds,
python=python,
typescript=typescript,
env=env,
)
self._owns_bash = True

Expand Down Expand Up @@ -226,13 +236,19 @@ def __init__(
max_commands: int | None = None,
max_loop_iterations: int | None = None,
timeout_seconds: float | None = None,
python: bool = False,
typescript: bool = False,
env: dict[str, str] | None = None,
):
self._bash = NativeBashTool(
username=username,
hostname=hostname,
max_commands=max_commands,
max_loop_iterations=max_loop_iterations,
timeout_seconds=timeout_seconds,
python=python,
typescript=typescript,
env=env,
)
self._id = f"bashkit-{uuid.uuid4().hex[:8]}"
self._operation_timeout = (
Expand Down
12 changes: 12 additions & 0 deletions crates/bashkit-python/bashkit/langchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ def __init__(
max_commands: int | None = None,
max_loop_iterations: int | None = None,
timeout_seconds: float | None = None,
python: bool = False,
typescript: bool = False,
env: dict[str, str] | None = None,
files: dict[str, str | Callable[[], str]] | None = None,
mounts: list[dict[str, Any]] | None = None,
allowed_mount_paths: list[str] | None = None,
Expand All @@ -106,6 +109,9 @@ def __init__(
max_commands=max_commands,
max_loop_iterations=max_loop_iterations,
timeout_seconds=timeout_seconds,
python=python,
typescript=typescript,
env=env,
files=files,
mounts=mounts,
allowed_mount_paths=allowed_mount_paths,
Expand Down Expand Up @@ -210,6 +216,9 @@ def create_bash_tool(
max_commands: int | None = None,
max_loop_iterations: int | None = None,
timeout_seconds: float | None = None,
python: bool = False,
typescript: bool = False,
env: dict[str, str] | None = None,
files: dict[str, str | Callable[[], str]] | None = None,
mounts: list[dict[str, Any]] | None = None,
allowed_mount_paths: list[str] | None = None,
Expand Down Expand Up @@ -259,6 +268,9 @@ def create_bash_tool(
max_commands=max_commands,
max_loop_iterations=max_loop_iterations,
timeout_seconds=timeout_seconds,
python=python,
typescript=typescript,
env=env,
files=files,
mounts=mounts,
allowed_mount_paths=allowed_mount_paths,
Expand Down
39 changes: 39 additions & 0 deletions crates/bashkit-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5564,6 +5564,9 @@ pub struct BashTool {
cwd: Option<String>,
/// Initial environment variables (mirrors `Bash::builder().env()`).
env: Option<HashMap<String, String>>,
/// Opt-in runtimes, replayed for resets, snapshots, and tool metadata.
python: bool,
typescript: bool,
/// Tracking list for `is_async` flag detection (used by session capture).
/// Kept in sync with `host_registry` by `add_builtin` / `remove_builtin`.
/// Wrapped in `Arc<StdMutex<_>>` so post-construction registration works
Expand Down Expand Up @@ -5607,6 +5610,12 @@ impl BashTool {
fn build_live_builder(&self, py: Python<'_>) -> PyResult<bashkit::BashBuilder> {
let profile = self.profile.core();
let mut builder = Bash::builder().profile(profile.clone());
if self.python {
builder = builder.python();
}
if self.typescript {
builder = builder.typescript();
}

if let Some(ref username) = self.username {
builder = builder.username(username);
Expand Down Expand Up @@ -5661,6 +5670,12 @@ impl BashTool {
fn build_rust_tool(&self) -> PyResult<RustBashTool> {
let profile = self.profile.core();
let mut builder = RustBashTool::builder().profile(profile.clone());
if self.python {
builder = builder.python();
}
if self.typescript {
builder = builder.typescript();
}

if let Some(ref username) = self.username {
builder = builder.username(username);
Expand Down Expand Up @@ -5728,6 +5743,8 @@ impl BashTool {
custom_builtins=None,
network=None,
profile=PyExecutionProfile::Standard,
python=false,
typescript=false,
))]
fn new(
py: Python<'_>,
Expand All @@ -5746,9 +5763,17 @@ impl BashTool {
custom_builtins: Option<&Bound<'_, PyDict>>,
network: Option<&Bound<'_, PyDict>>,
profile: PyExecutionProfile,
python: bool,
typescript: bool,
) -> PyResult<Self> {
let core_profile = profile.core();
let mut builder = Bash::builder().profile(core_profile.clone());
if python {
builder = builder.python();
}
if typescript {
builder = builder.typescript();
}

if let Some(ref u) = username {
builder = builder.username(u);
Expand Down Expand Up @@ -5817,6 +5842,8 @@ impl BashTool {
hostname,
cwd,
env,
python,
typescript,
custom_builtins: Arc::new(StdMutex::new(custom_builtins)),
runtime_env: Arc::new(StdMutex::new(Vec::new())),
runtime_mounts: Arc::new(StdMutex::new(Vec::new())),
Expand Down Expand Up @@ -6113,6 +6140,8 @@ impl BashTool {
custom_builtins=None,
network=None,
profile=PyExecutionProfile::Standard,
python=false,
typescript=false,
))]
#[allow(clippy::too_many_arguments)]
fn from_snapshot(
Expand All @@ -6133,6 +6162,8 @@ impl BashTool {
custom_builtins: Option<&Bound<'_, PyDict>>,
network: Option<&Bound<'_, PyDict>>,
profile: PyExecutionProfile,
python: bool,
typescript: bool,
) -> PyResult<Self> {
let tool = Self::new(
py,
Expand All @@ -6151,6 +6182,8 @@ impl BashTool {
custom_builtins,
network,
profile,
python,
typescript,
)?;
tool.restore_snapshot(py, data)?;
Ok(tool)
Expand All @@ -6176,6 +6209,8 @@ impl BashTool {
custom_builtins=None,
network=None,
profile=PyExecutionProfile::Standard,
python=false,
typescript=false,
))]
#[allow(clippy::too_many_arguments)]
fn from_snapshot_keyed(
Expand All @@ -6197,6 +6232,8 @@ impl BashTool {
custom_builtins: Option<&Bound<'_, PyDict>>,
network: Option<&Bound<'_, PyDict>>,
profile: PyExecutionProfile,
python: bool,
typescript: bool,
) -> PyResult<Self> {
let tool = Self::new(
py,
Expand All @@ -6215,6 +6252,8 @@ impl BashTool {
custom_builtins,
network,
profile,
python,
typescript,
)?;
tool.restore_snapshot_keyed(py, data, key)?;
Ok(tool)
Expand Down
Loading
Loading