diff --git a/.github/workflows/publish-python.yml b/.github/workflows/publish-python.yml index 6464508d..5163838d 100644 --- a/.github/workflows/publish-python.yml +++ b/.github/workflows/publish-python.yml @@ -1,4 +1,4 @@ -# PyPI publishing workflow for bashkit Python bindings +# PyPI publishing workflow for bashkit Python package # Builds pre-compiled wheels for all major platforms and publishes to PyPI. # Triggered alongside publish.yml on GitHub Release or manual dispatch. # Adapted from https://github.com/pydantic/monty CI wheel-building pattern. diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index abf58630..f323c1aa 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -1,4 +1,4 @@ -# CI for bashkit Python bindings +# CI for bashkit Python package # Builds the native extension via maturin and runs pytest on each PR. # Complements publish-python.yml (release-only) with per-PR validation. diff --git a/AGENTS.md b/AGENTS.md index 5ba0c8c5..7b923dcd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,7 +39,7 @@ Fix root cause. Unsure: read more code; if stuck, ask w/ short options. Unrecogn | 011-python-builtin | Embedded Python via Monty, security, resource limits | | 012-eval | LLM evaluation harness, dataset format, scoring | | 012-maintenance | Pre-release maintenance checklist | -| 013-python-package | Python bindings, PyPI wheels, platform matrix | +| 013-python-package | Python package, PyPI wheels, platform matrix | | 014-scripted-tool-orchestration | Compose ToolDef+callback pairs into OrchestratorTool via bash scripts | ### Documentation @@ -88,7 +88,7 @@ just pre-pr # Pre-PR checks ### Python -- Python bindings in `crates/bashkit-python/` +- Python package in `crates/bashkit-python/` - Linter/formatter: `ruff` (config in `pyproject.toml`) - `ruff check crates/bashkit-python` and `ruff format --check crates/bashkit-python` - Tests: `pytest crates/bashkit-python/tests/ -v` (requires `maturin develop` first) diff --git a/crates/bashkit-python/Cargo.toml b/crates/bashkit-python/Cargo.toml index d17b4997..0fd5aaa1 100644 --- a/crates/bashkit-python/Cargo.toml +++ b/crates/bashkit-python/Cargo.toml @@ -1,4 +1,4 @@ -# Python bindings for Bashkit +# Bashkit Python package # Exposes Bashkit as a Python module via PyO3 [package] @@ -8,7 +8,7 @@ edition.workspace = true license.workspace = true authors.workspace = true repository.workspace = true -description = "Python bindings for Bashkit virtual bash interpreter" +description = "A sandboxed bash interpreter for AI agents" [lib] crate-type = ["cdylib"] @@ -19,7 +19,7 @@ doc = false # Python extension, no Rust docs needed # Bashkit core bashkit = { path = "../bashkit", features = ["scripted_tool"] } -# PyO3 for Python bindings +# PyO3 native extension pyo3 = { workspace = true } pyo3-async-runtimes = { workspace = true } diff --git a/crates/bashkit-python/README.md b/crates/bashkit-python/README.md index a03565b3..46c86383 100644 --- a/crates/bashkit-python/README.md +++ b/crates/bashkit-python/README.md @@ -1,30 +1,36 @@ -# Bashkit Python Bindings +# Bashkit -Python bindings for [Bashkit](https://github.com/everruns/bashkit) - a virtual bash interpreter for AI agents. +A sandboxed bash interpreter for AI agents. + +```python +from bashkit import BashTool + +tool = BashTool() +result = tool.execute_sync("echo 'Hello, World!'") +print(result.stdout) # Hello, World! +``` ## Features -- **Sandboxed, in-process execution**: All commands run in isolation with a virtual filesystem -- **68+ built-in commands**: echo, cat, grep, sed, awk, jq, curl, find, and more -- **Full bash syntax**: Variables, pipelines, redirects, loops, functions, arrays -- **Resource limits**: Protect against infinite loops and runaway scripts -- **LangChain integration**: Ready-to-use tool for LangChain agents +- **Sandboxed execution** — all commands run in-process with a virtual filesystem, no containers needed +- **68+ built-in commands** — echo, cat, grep, sed, awk, jq, curl, find, and more +- **Full bash syntax** — variables, pipelines, redirects, loops, functions, arrays +- **Resource limits** — protect against infinite loops and runaway scripts +- **Framework integrations** — LangChain, PydanticAI, and Deep Agents ## Installation ```bash -# From PyPI (when published) pip install bashkit -# With LangChain support +# With framework support pip install 'bashkit[langchain]' - -# From source -pip install maturin -maturin develop +pip install 'bashkit[pydantic-ai]' ``` -## Quick Start +## Usage + +### Async ```python import asyncio @@ -51,29 +57,17 @@ async def main(): asyncio.run(main()) ``` -## LangChain Integration +### Sync ```python -from bashkit.langchain import create_bash_tool -from langchain.agents import create_agent - -# Create tool -bash_tool = create_bash_tool() - -# Create agent -agent = create_agent( - model="claude-sonnet-4-20250514", - tools=[bash_tool], - system_prompt="You are a helpful assistant with bash skills." -) +from bashkit import BashTool -# Run -result = agent.invoke({ - "messages": [{"role": "user", "content": "Create a file with today's date"}] -}) +tool = BashTool() +result = tool.execute_sync("echo 'Hello!'") +print(result.stdout) ``` -## Configuration +### Configuration ```python tool = BashTool( @@ -84,35 +78,68 @@ tool = BashTool( ) ``` -## Synchronous API +### Scripted Tool Orchestration + +Compose multiple tools into a single bash-scriptable interface: ```python -from bashkit import BashTool +from bashkit import ScriptedTool -tool = BashTool() -result = tool.execute_sync("echo 'Hello!'") -print(result.stdout) +tool = ScriptedTool("api") +tool.add_tool("greet", "Greet a user", callback=lambda p, s=None: f"hello {p.get('name', 'world')}") +result = tool.execute_sync("greet --name Alice") +print(result.stdout) # hello Alice +``` + +### LangChain + +```python +from bashkit.langchain import create_bash_tool + +bash_tool = create_bash_tool() +# Use with any LangChain agent +``` + +### PydanticAI + +```python +from bashkit.pydantic_ai import create_bash_tool + +bash_tool = create_bash_tool() +# Use with any PydanticAI agent ``` ## API Reference ### BashTool -- `execute(commands: str) -> ExecResult`: Execute commands asynchronously -- `execute_sync(commands: str) -> ExecResult`: Execute commands synchronously -- `description() -> str`: Get tool description -- `help() -> str`: Get LLM documentation -- `input_schema() -> str`: Get JSON input schema -- `output_schema() -> str`: Get JSON output schema +- `execute(commands: str) -> ExecResult` — execute commands asynchronously +- `execute_sync(commands: str) -> ExecResult` — execute commands synchronously +- `reset()` — reset interpreter state +- `description() -> str` — tool description for LLM integration +- `help() -> str` — detailed documentation +- `input_schema() -> str` — JSON input schema +- `output_schema() -> str` — JSON output schema ### ExecResult -- `stdout: str`: Standard output -- `stderr: str`: Standard error -- `exit_code: int`: Exit code (0 = success) -- `error: Optional[str]`: Error message if execution failed -- `success: bool`: True if exit_code == 0 -- `to_dict() -> dict`: Convert to dictionary +- `stdout: str` — standard output +- `stderr: str` — standard error +- `exit_code: int` — exit code (0 = success) +- `error: Optional[str]` — error message if execution failed +- `success: bool` — True if exit_code == 0 +- `to_dict() -> dict` — convert to dictionary + +### ScriptedTool + +- `add_tool(name, description, callback, schema=None)` — register a tool +- `execute(script: str) -> ExecResult` — execute script asynchronously +- `execute_sync(script: str) -> ExecResult` — execute script synchronously +- `env(key: str, value: str)` — set environment variable + +## How it works + +Bashkit is built on top of [Bashkit core](https://github.com/everruns/bashkit), a bash interpreter written in Rust. The Python package provides a native extension for fast, sandboxed execution without spawning subprocesses or containers. ## License diff --git a/crates/bashkit-python/bashkit/__init__.py b/crates/bashkit-python/bashkit/__init__.py index f5fa7c14..171b2c3c 100644 --- a/crates/bashkit-python/bashkit/__init__.py +++ b/crates/bashkit-python/bashkit/__init__.py @@ -1,12 +1,10 @@ """ -Bashkit Python Bindings - -A sandboxed bash interpreter for AI agents with virtual filesystem. +Bashkit — a sandboxed bash interpreter for AI agents. Example: >>> from bashkit import BashTool >>> tool = BashTool() - >>> result = await tool.execute("echo 'Hello, World!'") + >>> result = tool.execute_sync("echo 'Hello, World!'") >>> print(result.stdout) Hello, World! diff --git a/crates/bashkit-python/pyproject.toml b/crates/bashkit-python/pyproject.toml index 976da593..50c1b6b1 100644 --- a/crates/bashkit-python/pyproject.toml +++ b/crates/bashkit-python/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "maturin" [project] name = "bashkit" dynamic = ["version"] -description = "Python bindings for Bashkit - a sandboxed bash interpreter for AI agents" +description = "A sandboxed bash interpreter for AI agents" readme = "README.md" license = { text = "MIT" } requires-python = ">=3.9" diff --git a/crates/bashkit-python/src/lib.rs b/crates/bashkit-python/src/lib.rs index f7c7da65..1a0451bf 100644 --- a/crates/bashkit-python/src/lib.rs +++ b/crates/bashkit-python/src/lib.rs @@ -1,4 +1,4 @@ -//! Python bindings for Bashkit +//! Bashkit Python package //! //! Exposes the Bash interpreter and ScriptedTool as Python classes for use in //! AI agent frameworks. BashTool provides stateful execution (filesystem persists diff --git a/crates/bashkit-python/tests/test_bashkit.py b/crates/bashkit-python/tests/test_bashkit.py index 7ae08162..ebf02b96 100644 --- a/crates/bashkit-python/tests/test_bashkit.py +++ b/crates/bashkit-python/tests/test_bashkit.py @@ -1,4 +1,4 @@ -"""Tests for bashkit Python bindings.""" +"""Tests for bashkit Python package.""" import json diff --git a/specs/001-architecture.md b/specs/001-architecture.md index 82b3b525..49d94da1 100644 --- a/specs/001-architecture.md +++ b/specs/001-architecture.md @@ -118,7 +118,7 @@ impl BashTool { ### Single crate vs workspace Rejected single crate because: - CLI binary would bloat the library -- Future Python bindings need separate crate +- Python package needs separate crate - Cleaner separation of concerns ### Sync vs async filesystem diff --git a/specs/006-threat-model.md b/specs/006-threat-model.md index ea553f13..47955001 100644 --- a/specs/006-threat-model.md +++ b/specs/006-threat-model.md @@ -126,7 +126,7 @@ the session-level backstop. | TM-DOS-037 | OverlayFs chmod CoW bypass | `chmod` copy-on-write writes to unlimited upper layer, bypassing overlay limits | — | **OPEN** | | TM-DOS-038 | OverlayFs incomplete recursive whiteout | `rm -r /dir` only whiteouts directory, not children; lower layer files remain visible | — | **OPEN** | | TM-DOS-039 | Missing validate_path in VFS methods | `remove`, `stat`, `read_dir`, `copy`, `rename`, `symlink`, `chmod` skip `validate_path()` | — | **OPEN** | -| TM-DOS-040 | Integer truncation on 32-bit | `u64 as usize` casts in network/Python bindings silently truncate on 32-bit, bypassing size checks | — | **OPEN** | +| TM-DOS-040 | Integer truncation on 32-bit | `u64 as usize` casts in network/Python extension silently truncate on 32-bit, bypassing size checks | — | **OPEN** | **TM-DOS-034**: `InMemoryFs::append_file()` (line 816-896) reads under a read lock, drops it, checks limits with stale data, then acquires write lock. Fix: single write lock for whole operation. diff --git a/specs/008-release-process.md b/specs/008-release-process.md index 4afff9a2..d4cbc3cd 100644 --- a/specs/008-release-process.md +++ b/specs/008-release-process.md @@ -145,7 +145,7 @@ Example: - `bashkit` on crates.io (core library) - `bashkit-cli` on crates.io (CLI tool) -- `bashkit` on PyPI (Python bindings, pre-built wheels) +- `bashkit` on PyPI (Python package, pre-built wheels) ## Publishing Order diff --git a/specs/013-python-package.md b/specs/013-python-package.md index 9eac2e02..b2c6796d 100644 --- a/specs/013-python-package.md +++ b/specs/013-python-package.md @@ -2,7 +2,7 @@ ## Abstract -Bashkit ships Python bindings as pre-built binary wheels on PyPI. Users install with +Bashkit ships a Python package as pre-built binary wheels on PyPI. Users install with `pip install bashkit` and get a native extension — no Rust toolchain needed. ## Package Layout @@ -11,7 +11,7 @@ Bashkit ships Python bindings as pre-built binary wheels on PyPI. Users install crates/bashkit-python/ ├── Cargo.toml # Rust crate (cdylib via PyO3) ├── pyproject.toml # Python package metadata (maturin build backend) -├── src/lib.rs # PyO3 bindings (BashTool, ExecResult) +├── src/lib.rs # PyO3 native module (BashTool, ExecResult) ├── bashkit/ │ ├── __init__.py # Re-exports from native module │ ├── _bashkit.pyi # Type stubs (PEP 561) @@ -20,13 +20,13 @@ crates/bashkit-python/ │ ├── deepagents.py # Deep Agents integration │ └── pydantic_ai.py # PydanticAI integration └── tests/ - └── test_bashkit.py # Pytest suite for bindings + └── test_bashkit.py # Pytest suite ``` ## Build System - **Build backend**: [maturin](https://github.com/PyO3/maturin) (1.4–2.0) -- **Rust bindings**: [PyO3](https://pyo3.rs/) 0.24 with `extension-module` feature +- **Rust extension**: [PyO3](https://pyo3.rs/) 0.24 with `extension-module` feature - **Async bridge**: `pyo3-async-runtimes` (tokio runtime) - **Module name**: `bashkit._bashkit` (native), re-exported as `bashkit` @@ -196,7 +196,7 @@ ruff format . # format ## Design Decisions - **No PGO**: Profile-guided optimization adds build complexity for minimal gain. - Bashkit is a thin PyO3 wrapper — hot paths are in Rust, not Python dispatch. + Bashkit is a thin PyO3 extension — hot paths are in Rust, not Python dispatch. Can revisit if profiling shows benefit. - **No exotic architectures**: armv7, ppc64le, s390x, i686 omitted. Target audience is AI agent developers on standard server/desktop platforms.