bashkit 0.1.4__cp39-cp39-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bashkit/__init__.py +33 -0
- bashkit/_bashkit.cp39-win_amd64.pyd +0 -0
- bashkit/_bashkit.pyi +101 -0
- bashkit/deepagents.py +338 -0
- bashkit/langchain.py +167 -0
- bashkit/py.typed +0 -0
- bashkit-0.1.4.dist-info/METADATA +150 -0
- bashkit-0.1.4.dist-info/RECORD +9 -0
- bashkit-0.1.4.dist-info/WHEEL +4 -0
bashkit/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bashkit Python Bindings
|
|
3
|
+
|
|
4
|
+
A sandboxed bash interpreter for AI agents with virtual filesystem.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from bashkit import BashTool
|
|
8
|
+
>>> tool = BashTool()
|
|
9
|
+
>>> result = await tool.execute("echo 'Hello, World!'")
|
|
10
|
+
>>> print(result.stdout)
|
|
11
|
+
Hello, World!
|
|
12
|
+
|
|
13
|
+
For LangChain integration:
|
|
14
|
+
>>> from bashkit.langchain import create_bash_tool
|
|
15
|
+
>>> tool = create_bash_tool()
|
|
16
|
+
|
|
17
|
+
For Deep Agents integration:
|
|
18
|
+
>>> from bashkit.deepagents import create_bash_middleware
|
|
19
|
+
>>> middleware = create_bash_middleware()
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from bashkit._bashkit import (
|
|
23
|
+
BashTool,
|
|
24
|
+
ExecResult,
|
|
25
|
+
create_langchain_tool_spec,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__version__ = "0.1.2"
|
|
29
|
+
__all__ = [
|
|
30
|
+
"BashTool",
|
|
31
|
+
"ExecResult",
|
|
32
|
+
"create_langchain_tool_spec",
|
|
33
|
+
]
|
|
Binary file
|
bashkit/_bashkit.pyi
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Type stubs for bashkit_py native module."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
class ExecResult:
|
|
6
|
+
"""Result from executing bash commands."""
|
|
7
|
+
|
|
8
|
+
stdout: str
|
|
9
|
+
stderr: str
|
|
10
|
+
exit_code: int
|
|
11
|
+
error: Optional[str]
|
|
12
|
+
success: bool
|
|
13
|
+
|
|
14
|
+
def to_dict(self) -> dict[str, any]: ...
|
|
15
|
+
|
|
16
|
+
class BashTool:
|
|
17
|
+
"""Sandboxed bash interpreter for AI agents.
|
|
18
|
+
|
|
19
|
+
BashTool provides a safe execution environment for running bash commands
|
|
20
|
+
with a virtual filesystem. All file operations are contained within the
|
|
21
|
+
sandbox - no access to the real filesystem.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
>>> tool = BashTool()
|
|
25
|
+
>>> result = await tool.execute("echo 'Hello!'")
|
|
26
|
+
>>> print(result.stdout)
|
|
27
|
+
Hello!
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
name: str
|
|
31
|
+
short_description: str
|
|
32
|
+
version: str
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
username: Optional[str] = None,
|
|
37
|
+
hostname: Optional[str] = None,
|
|
38
|
+
max_commands: Optional[int] = None,
|
|
39
|
+
max_loop_iterations: Optional[int] = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Create a new BashTool instance.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
username: Custom username for sandbox (default: "user")
|
|
45
|
+
hostname: Custom hostname for sandbox (default: "sandbox")
|
|
46
|
+
max_commands: Maximum commands to execute (default: 10000)
|
|
47
|
+
max_loop_iterations: Maximum loop iterations (default: 100000)
|
|
48
|
+
"""
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
async def execute(self, commands: str) -> ExecResult:
|
|
52
|
+
"""Execute bash commands asynchronously.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
commands: Bash commands to execute (like `bash -c "commands"`)
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
ExecResult with stdout, stderr, exit_code
|
|
59
|
+
"""
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
def execute_sync(self, commands: str) -> ExecResult:
|
|
63
|
+
"""Execute bash commands synchronously (blocking).
|
|
64
|
+
|
|
65
|
+
Note: Prefer `execute()` for async contexts. This method blocks.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
commands: Bash commands to execute
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
ExecResult with stdout, stderr, exit_code
|
|
72
|
+
"""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
def description(self) -> str:
|
|
76
|
+
"""Get the full description."""
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
def help(self) -> str:
|
|
80
|
+
"""Get LLM documentation."""
|
|
81
|
+
...
|
|
82
|
+
|
|
83
|
+
def system_prompt(self) -> str:
|
|
84
|
+
"""Get system prompt for LLMs."""
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
def input_schema(self) -> str:
|
|
88
|
+
"""Get JSON schema for input validation."""
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
def output_schema(self) -> str:
|
|
92
|
+
"""Get JSON schema for output."""
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
def create_langchain_tool_spec() -> dict[str, any]:
|
|
96
|
+
"""Create a LangChain-compatible tool specification.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Dict with name, description, and args_schema
|
|
100
|
+
"""
|
|
101
|
+
...
|
bashkit/deepagents.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Deep Agents integration for Bashkit.
|
|
3
|
+
|
|
4
|
+
Provides middleware and backend for Deep Agents using Bashkit's VFS:
|
|
5
|
+
- BashkitMiddleware: Adds `bash` tool via AgentMiddleware.tools
|
|
6
|
+
- BashkitBackend: SandboxBackendProtocol for execute/read_file/write_file/etc.
|
|
7
|
+
|
|
8
|
+
Use together for shared VFS:
|
|
9
|
+
>>> backend = BashkitBackend()
|
|
10
|
+
>>> middleware = backend.create_middleware() # shares VFS
|
|
11
|
+
>>> agent = create_deep_agent(backend=backend, middleware=[middleware])
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import uuid
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from typing import Optional, TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from bashkit import BashTool as NativeBashTool
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
# Check for deepagents availability
|
|
26
|
+
try:
|
|
27
|
+
from deepagents.backends.protocol import (
|
|
28
|
+
SandboxBackendProtocol,
|
|
29
|
+
ExecuteResponse,
|
|
30
|
+
FileInfo,
|
|
31
|
+
GrepMatch,
|
|
32
|
+
EditResult,
|
|
33
|
+
WriteResult,
|
|
34
|
+
FileDownloadResponse,
|
|
35
|
+
FileUploadResponse,
|
|
36
|
+
)
|
|
37
|
+
from langchain.agents.middleware.types import AgentMiddleware
|
|
38
|
+
from langchain_core.tools import tool as langchain_tool
|
|
39
|
+
|
|
40
|
+
DEEPAGENTS_AVAILABLE = True
|
|
41
|
+
except ImportError:
|
|
42
|
+
DEEPAGENTS_AVAILABLE = False
|
|
43
|
+
SandboxBackendProtocol = object
|
|
44
|
+
AgentMiddleware = object
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _now_iso() -> str:
|
|
48
|
+
return datetime.now(timezone.utc).isoformat()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _make_bash_tool(bash_instance: NativeBashTool):
|
|
52
|
+
"""Create a bash tool function from a BashTool instance."""
|
|
53
|
+
# Use name and description from bashkit lib
|
|
54
|
+
tool_name = bash_instance.name
|
|
55
|
+
tool_description = bash_instance.description()
|
|
56
|
+
|
|
57
|
+
@langchain_tool(tool_name, description=tool_description)
|
|
58
|
+
def bashkit(command: str) -> str:
|
|
59
|
+
result = bash_instance.execute_sync(command)
|
|
60
|
+
output = result.stdout
|
|
61
|
+
if result.stderr:
|
|
62
|
+
output += f"\n{result.stderr}"
|
|
63
|
+
if result.exit_code != 0:
|
|
64
|
+
output += f"\n[Exit code: {result.exit_code}]"
|
|
65
|
+
return output.strip() if output else "[No output]"
|
|
66
|
+
|
|
67
|
+
return bashkit
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if DEEPAGENTS_AVAILABLE:
|
|
71
|
+
|
|
72
|
+
class BashkitMiddleware(AgentMiddleware):
|
|
73
|
+
"""Middleware that adds `bash` tool for shell execution in VFS.
|
|
74
|
+
|
|
75
|
+
Example standalone:
|
|
76
|
+
>>> middleware = BashkitMiddleware()
|
|
77
|
+
>>> agent = create_deep_agent(middleware=[middleware])
|
|
78
|
+
|
|
79
|
+
Example with shared VFS (recommended):
|
|
80
|
+
>>> backend = BashkitBackend()
|
|
81
|
+
>>> middleware = backend.create_middleware()
|
|
82
|
+
>>> agent = create_deep_agent(backend=backend, middleware=[middleware])
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
bash_tool: Optional[NativeBashTool] = None,
|
|
88
|
+
username: Optional[str] = None,
|
|
89
|
+
hostname: Optional[str] = None,
|
|
90
|
+
max_commands: Optional[int] = None,
|
|
91
|
+
max_loop_iterations: Optional[int] = None,
|
|
92
|
+
):
|
|
93
|
+
"""Initialize middleware.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
bash_tool: Existing BashTool to use (for shared VFS)
|
|
97
|
+
username: Username for new BashTool (ignored if bash_tool provided)
|
|
98
|
+
hostname: Hostname for new BashTool (ignored if bash_tool provided)
|
|
99
|
+
max_commands: Max commands (ignored if bash_tool provided)
|
|
100
|
+
max_loop_iterations: Max iterations (ignored if bash_tool provided)
|
|
101
|
+
"""
|
|
102
|
+
if bash_tool is not None:
|
|
103
|
+
self._bash = bash_tool
|
|
104
|
+
self._owns_bash = False
|
|
105
|
+
else:
|
|
106
|
+
self._bash = NativeBashTool(
|
|
107
|
+
username=username,
|
|
108
|
+
hostname=hostname,
|
|
109
|
+
max_commands=max_commands,
|
|
110
|
+
max_loop_iterations=max_loop_iterations,
|
|
111
|
+
)
|
|
112
|
+
self._owns_bash = True
|
|
113
|
+
|
|
114
|
+
self._tools = [_make_bash_tool(self._bash)]
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def tools(self):
|
|
118
|
+
"""Tools provided by this middleware."""
|
|
119
|
+
return self._tools
|
|
120
|
+
|
|
121
|
+
def execute_sync(self, command: str) -> str:
|
|
122
|
+
"""Execute command synchronously (for setup scripts)."""
|
|
123
|
+
result = self._bash.execute_sync(command)
|
|
124
|
+
return result.stdout + (result.stderr or "")
|
|
125
|
+
|
|
126
|
+
def reset(self) -> None:
|
|
127
|
+
"""Reset VFS to initial state."""
|
|
128
|
+
if self._owns_bash:
|
|
129
|
+
self._bash.reset()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class BashkitBackend(SandboxBackendProtocol):
|
|
133
|
+
"""Backend implementing SandboxBackendProtocol with Bashkit VFS.
|
|
134
|
+
|
|
135
|
+
Provides execute, read_file, write_file, edit_file, ls, glob, grep
|
|
136
|
+
all operating on the same virtual filesystem.
|
|
137
|
+
|
|
138
|
+
Example:
|
|
139
|
+
>>> backend = BashkitBackend()
|
|
140
|
+
>>> agent = create_deep_agent(backend=backend)
|
|
141
|
+
|
|
142
|
+
With middleware for additional `bash` tool:
|
|
143
|
+
>>> backend = BashkitBackend()
|
|
144
|
+
>>> middleware = backend.create_middleware()
|
|
145
|
+
>>> agent = create_deep_agent(backend=backend, middleware=[middleware])
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(
|
|
149
|
+
self,
|
|
150
|
+
username: Optional[str] = None,
|
|
151
|
+
hostname: Optional[str] = None,
|
|
152
|
+
max_commands: Optional[int] = None,
|
|
153
|
+
max_loop_iterations: Optional[int] = None,
|
|
154
|
+
):
|
|
155
|
+
self._bash = NativeBashTool(
|
|
156
|
+
username=username,
|
|
157
|
+
hostname=hostname,
|
|
158
|
+
max_commands=max_commands,
|
|
159
|
+
max_loop_iterations=max_loop_iterations,
|
|
160
|
+
)
|
|
161
|
+
self._id = f"bashkit-{uuid.uuid4().hex[:8]}"
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def id(self) -> str:
|
|
165
|
+
return self._id
|
|
166
|
+
|
|
167
|
+
def create_middleware(self) -> BashkitMiddleware:
|
|
168
|
+
"""Create middleware that shares this backend's VFS.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
BashkitMiddleware using same BashTool instance
|
|
172
|
+
"""
|
|
173
|
+
return BashkitMiddleware(bash_tool=self._bash)
|
|
174
|
+
|
|
175
|
+
# === Shell Execution ===
|
|
176
|
+
|
|
177
|
+
def execute(self, command: str) -> ExecuteResponse:
|
|
178
|
+
result = self._bash.execute_sync(command)
|
|
179
|
+
output = result.stdout + (result.stderr or "")
|
|
180
|
+
return ExecuteResponse(output=output, exit_code=result.exit_code, truncated=False)
|
|
181
|
+
|
|
182
|
+
async def aexecute(self, command: str) -> ExecuteResponse:
|
|
183
|
+
return self.execute(command)
|
|
184
|
+
|
|
185
|
+
# === File Operations ===
|
|
186
|
+
|
|
187
|
+
def read(self, file_path: str, offset: int = 0, limit: int = 2000) -> str:
|
|
188
|
+
result = self._bash.execute_sync(f"cat {file_path}")
|
|
189
|
+
if result.exit_code != 0:
|
|
190
|
+
return f"Error: {result.stderr or 'File not found'}"
|
|
191
|
+
lines = result.stdout.splitlines()
|
|
192
|
+
selected = lines[offset:offset + limit]
|
|
193
|
+
return "\n".join(f"{i:6d}\t{line}" for i, line in enumerate(selected, start=offset + 1))
|
|
194
|
+
|
|
195
|
+
async def aread(self, file_path: str, offset: int = 0, limit: int = 2000) -> str:
|
|
196
|
+
return self.read(file_path, offset, limit)
|
|
197
|
+
|
|
198
|
+
def write(self, file_path: str, content: str) -> WriteResult:
|
|
199
|
+
cmd = f"cat > {file_path} << 'BASHKIT_EOF'\n{content}\nBASHKIT_EOF"
|
|
200
|
+
result = self._bash.execute_sync(cmd)
|
|
201
|
+
return WriteResult(error=result.stderr if result.exit_code != 0 else None, path=file_path)
|
|
202
|
+
|
|
203
|
+
async def awrite(self, file_path: str, content: str) -> WriteResult:
|
|
204
|
+
return self.write(file_path, content)
|
|
205
|
+
|
|
206
|
+
def edit(self, file_path: str, old_string: str, new_string: str, replace_all: bool = False) -> EditResult:
|
|
207
|
+
result = self._bash.execute_sync(f"cat {file_path}")
|
|
208
|
+
if result.exit_code != 0:
|
|
209
|
+
return EditResult(error=f"File not found: {file_path}")
|
|
210
|
+
content = result.stdout
|
|
211
|
+
count = content.count(old_string)
|
|
212
|
+
if count == 0:
|
|
213
|
+
return EditResult(error="old_string not found")
|
|
214
|
+
if count > 1 and not replace_all:
|
|
215
|
+
return EditResult(error=f"Found {count} times. Use replace_all=True")
|
|
216
|
+
new_content = content.replace(old_string, new_string) if replace_all else content.replace(old_string, new_string, 1)
|
|
217
|
+
wr = self.write(file_path, new_content)
|
|
218
|
+
return EditResult(error=wr.error, path=file_path)
|
|
219
|
+
|
|
220
|
+
async def aedit(self, file_path: str, old_string: str, new_string: str, replace_all: bool = False) -> EditResult:
|
|
221
|
+
return self.edit(file_path, old_string, new_string, replace_all)
|
|
222
|
+
|
|
223
|
+
# === File Discovery ===
|
|
224
|
+
|
|
225
|
+
def ls_info(self, path: str) -> list[FileInfo]:
|
|
226
|
+
result = self._bash.execute_sync(f"ls -la {path}")
|
|
227
|
+
if result.exit_code != 0:
|
|
228
|
+
return []
|
|
229
|
+
files = []
|
|
230
|
+
for line in result.stdout.splitlines():
|
|
231
|
+
parts = line.split()
|
|
232
|
+
if len(parts) < 9 or parts[0].startswith("total"):
|
|
233
|
+
continue
|
|
234
|
+
name = " ".join(parts[8:])
|
|
235
|
+
if name in (".", ".."):
|
|
236
|
+
continue
|
|
237
|
+
files.append(FileInfo(
|
|
238
|
+
path=f"{path.rstrip('/')}/{name}", name=name,
|
|
239
|
+
is_dir=parts[0].startswith("d"),
|
|
240
|
+
size=int(parts[4]) if parts[4].isdigit() else 0,
|
|
241
|
+
created_at=_now_iso(), modified_at=_now_iso(),
|
|
242
|
+
))
|
|
243
|
+
return files
|
|
244
|
+
|
|
245
|
+
async def als_info(self, path: str) -> list[FileInfo]:
|
|
246
|
+
return self.ls_info(path)
|
|
247
|
+
|
|
248
|
+
def glob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
|
|
249
|
+
name_pattern = pattern.replace("**/", "").replace("**", "*") if "**" in pattern else pattern
|
|
250
|
+
result = self._bash.execute_sync(f"find {path} -name '{name_pattern}' -type f")
|
|
251
|
+
if result.exit_code != 0:
|
|
252
|
+
return []
|
|
253
|
+
return [
|
|
254
|
+
FileInfo(path=p.strip(), name=p.strip().split("/")[-1], is_dir=False, size=0, created_at=_now_iso(), modified_at=_now_iso())
|
|
255
|
+
for p in result.stdout.splitlines() if p.strip()
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
async def aglob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
|
|
259
|
+
return self.glob_info(pattern, path)
|
|
260
|
+
|
|
261
|
+
def grep_raw(self, pattern: str, path: str | None = None, glob: str | None = None) -> list[GrepMatch] | str:
|
|
262
|
+
cmd = f"grep -rn '{pattern}' {path}" if path else f"grep -rn '{pattern}' /home"
|
|
263
|
+
result = self._bash.execute_sync(cmd)
|
|
264
|
+
matches = []
|
|
265
|
+
for line in result.stdout.splitlines():
|
|
266
|
+
if ":" not in line:
|
|
267
|
+
continue
|
|
268
|
+
parts = line.split(":", 2)
|
|
269
|
+
if len(parts) >= 3:
|
|
270
|
+
try:
|
|
271
|
+
matches.append(GrepMatch(path=parts[0], line_number=int(parts[1]), content=parts[2]))
|
|
272
|
+
except ValueError:
|
|
273
|
+
continue
|
|
274
|
+
return matches
|
|
275
|
+
|
|
276
|
+
async def agrep_raw(self, pattern: str, path: str | None = None, glob: str | None = None) -> list[GrepMatch] | str:
|
|
277
|
+
return self.grep_raw(pattern, path, glob)
|
|
278
|
+
|
|
279
|
+
# === File Transfer ===
|
|
280
|
+
|
|
281
|
+
def download_files(self, paths: list[str]) -> list[FileDownloadResponse]:
|
|
282
|
+
responses = []
|
|
283
|
+
for p in paths:
|
|
284
|
+
result = self._bash.execute_sync(f"cat {p}")
|
|
285
|
+
if result.exit_code == 0:
|
|
286
|
+
responses.append(FileDownloadResponse(path=p, content=result.stdout.encode(), error=None))
|
|
287
|
+
else:
|
|
288
|
+
responses.append(FileDownloadResponse(path=p, content=None, error=result.stderr or "File not found"))
|
|
289
|
+
return responses
|
|
290
|
+
|
|
291
|
+
async def adownload_files(self, paths: list[str]) -> list[FileDownloadResponse]:
|
|
292
|
+
return self.download_files(paths)
|
|
293
|
+
|
|
294
|
+
def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
|
|
295
|
+
responses = []
|
|
296
|
+
for p, content in files:
|
|
297
|
+
try:
|
|
298
|
+
wr = self.write(p, content.decode("utf-8"))
|
|
299
|
+
responses.append(FileUploadResponse(path=p, error=None if wr.success else wr.error))
|
|
300
|
+
except UnicodeDecodeError:
|
|
301
|
+
responses.append(FileUploadResponse(path=p, error="Binary files not supported"))
|
|
302
|
+
return responses
|
|
303
|
+
|
|
304
|
+
async def aupload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
|
|
305
|
+
return self.upload_files(files)
|
|
306
|
+
|
|
307
|
+
# === Utility ===
|
|
308
|
+
|
|
309
|
+
def setup(self, script: str) -> str:
|
|
310
|
+
"""Execute setup script."""
|
|
311
|
+
result = self._bash.execute_sync(script)
|
|
312
|
+
return result.stdout + (result.stderr or "")
|
|
313
|
+
|
|
314
|
+
def reset(self) -> None:
|
|
315
|
+
"""Reset VFS."""
|
|
316
|
+
self._bash.reset()
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def create_bash_middleware(**kwargs) -> "BashkitMiddleware":
|
|
320
|
+
"""Create BashkitMiddleware for Deep Agents."""
|
|
321
|
+
if not DEEPAGENTS_AVAILABLE:
|
|
322
|
+
raise ImportError("deepagents required. Install: pip install 'bashkit[deepagents]'")
|
|
323
|
+
return BashkitMiddleware(**kwargs)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def create_bashkit_backend(**kwargs) -> "BashkitBackend":
|
|
327
|
+
"""Create BashkitBackend for Deep Agents."""
|
|
328
|
+
if not DEEPAGENTS_AVAILABLE:
|
|
329
|
+
raise ImportError("deepagents required. Install: pip install 'bashkit[deepagents]'")
|
|
330
|
+
return BashkitBackend(**kwargs)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
__all__ = [
|
|
334
|
+
"BashkitMiddleware",
|
|
335
|
+
"BashkitBackend",
|
|
336
|
+
"create_bash_middleware",
|
|
337
|
+
"create_bashkit_backend",
|
|
338
|
+
]
|
bashkit/langchain.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LangChain integration for Bashkit.
|
|
3
|
+
|
|
4
|
+
Provides a LangChain-compatible tool that wraps BashTool for use with
|
|
5
|
+
LangChain agents and chains.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from bashkit.langchain import create_bash_tool
|
|
9
|
+
>>> from langchain.agents import create_agent
|
|
10
|
+
>>>
|
|
11
|
+
>>> tool = create_bash_tool()
|
|
12
|
+
>>> agent = create_agent(model="claude-sonnet-4-20250514", tools=[tool])
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
from typing import Optional, Type
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from langchain_core.tools import BaseTool, ToolException
|
|
22
|
+
from pydantic import BaseModel, Field, PrivateAttr
|
|
23
|
+
|
|
24
|
+
LANGCHAIN_AVAILABLE = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
LANGCHAIN_AVAILABLE = False
|
|
27
|
+
BaseTool = object
|
|
28
|
+
BaseModel = object
|
|
29
|
+
|
|
30
|
+
def Field(*args, **kwargs):
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
def PrivateAttr(*args, **kwargs):
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
from bashkit import BashTool as NativeBashTool
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BashToolInput(BaseModel):
|
|
41
|
+
"""Input schema for BashTool."""
|
|
42
|
+
|
|
43
|
+
commands: str = Field(
|
|
44
|
+
description="Bash commands to execute (like `bash -c 'commands'`)"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if LANGCHAIN_AVAILABLE:
|
|
49
|
+
|
|
50
|
+
class BashkitTool(BaseTool):
|
|
51
|
+
"""LangChain tool wrapper for Bashkit sandboxed bash interpreter.
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
>>> tool = BashkitTool()
|
|
55
|
+
>>> result = tool.invoke({"commands": "echo 'Hello!'"})
|
|
56
|
+
>>> print(result) # Hello!
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
name: str = "" # Set in __init__ from bashkit
|
|
60
|
+
description: str = "" # Set in __init__ from bashkit
|
|
61
|
+
args_schema: Type[BaseModel] = BashToolInput
|
|
62
|
+
handle_tool_error: bool = True
|
|
63
|
+
|
|
64
|
+
# Internal state - use PrivateAttr for pydantic v2 compatibility
|
|
65
|
+
_bash_tool: NativeBashTool = PrivateAttr()
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
username: Optional[str] = None,
|
|
70
|
+
hostname: Optional[str] = None,
|
|
71
|
+
max_commands: Optional[int] = None,
|
|
72
|
+
max_loop_iterations: Optional[int] = None,
|
|
73
|
+
**kwargs,
|
|
74
|
+
):
|
|
75
|
+
"""Initialize BashkitTool.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
username: Custom username for sandbox
|
|
79
|
+
hostname: Custom hostname for sandbox
|
|
80
|
+
max_commands: Max commands to execute
|
|
81
|
+
max_loop_iterations: Max loop iterations
|
|
82
|
+
"""
|
|
83
|
+
bash_tool = NativeBashTool(
|
|
84
|
+
username=username,
|
|
85
|
+
hostname=hostname,
|
|
86
|
+
max_commands=max_commands,
|
|
87
|
+
max_loop_iterations=max_loop_iterations,
|
|
88
|
+
)
|
|
89
|
+
# Use name and description from bashkit lib
|
|
90
|
+
kwargs["name"] = bash_tool.name
|
|
91
|
+
kwargs["description"] = bash_tool.description()
|
|
92
|
+
super().__init__(**kwargs)
|
|
93
|
+
object.__setattr__(self, "_bash_tool", bash_tool)
|
|
94
|
+
|
|
95
|
+
def _run(self, commands: str) -> str:
|
|
96
|
+
"""Execute bash commands synchronously."""
|
|
97
|
+
result = self._bash_tool.execute_sync(commands)
|
|
98
|
+
|
|
99
|
+
if result.error:
|
|
100
|
+
raise ToolException(f"Execution error: {result.error}")
|
|
101
|
+
|
|
102
|
+
# Return combined output for the agent
|
|
103
|
+
output = result.stdout
|
|
104
|
+
if result.stderr:
|
|
105
|
+
output += f"\nSTDERR: {result.stderr}"
|
|
106
|
+
if result.exit_code != 0:
|
|
107
|
+
output += f"\n[Exit code: {result.exit_code}]"
|
|
108
|
+
|
|
109
|
+
return output
|
|
110
|
+
|
|
111
|
+
async def _arun(self, commands: str) -> str:
|
|
112
|
+
"""Execute bash commands asynchronously."""
|
|
113
|
+
result = await self._bash_tool.execute(commands)
|
|
114
|
+
|
|
115
|
+
if result.error:
|
|
116
|
+
raise ToolException(f"Execution error: {result.error}")
|
|
117
|
+
|
|
118
|
+
# Return combined output for the agent
|
|
119
|
+
output = result.stdout
|
|
120
|
+
if result.stderr:
|
|
121
|
+
output += f"\nSTDERR: {result.stderr}"
|
|
122
|
+
if result.exit_code != 0:
|
|
123
|
+
output += f"\n[Exit code: {result.exit_code}]"
|
|
124
|
+
|
|
125
|
+
return output
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def create_bash_tool(
|
|
129
|
+
username: Optional[str] = None,
|
|
130
|
+
hostname: Optional[str] = None,
|
|
131
|
+
max_commands: Optional[int] = None,
|
|
132
|
+
max_loop_iterations: Optional[int] = None,
|
|
133
|
+
) -> "BashkitTool":
|
|
134
|
+
"""Create a LangChain-compatible Bashkit tool.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
username: Custom username for sandbox
|
|
138
|
+
hostname: Custom hostname for sandbox
|
|
139
|
+
max_commands: Max commands to execute
|
|
140
|
+
max_loop_iterations: Max loop iterations
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
BashkitTool instance for use with LangChain agents
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
ImportError: If langchain-core is not installed
|
|
147
|
+
|
|
148
|
+
Example:
|
|
149
|
+
>>> from bashkit.langchain import create_bash_tool
|
|
150
|
+
>>> tool = create_bash_tool()
|
|
151
|
+
>>> result = tool.invoke({"commands": "ls -la"})
|
|
152
|
+
"""
|
|
153
|
+
if not LANGCHAIN_AVAILABLE:
|
|
154
|
+
raise ImportError(
|
|
155
|
+
"langchain-core is required for LangChain integration. "
|
|
156
|
+
"Install with: pip install 'bashkit[langchain]'"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return BashkitTool(
|
|
160
|
+
username=username,
|
|
161
|
+
hostname=hostname,
|
|
162
|
+
max_commands=max_commands,
|
|
163
|
+
max_loop_iterations=max_loop_iterations,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
__all__ = ["BashkitTool", "BashToolInput", "create_bash_tool"]
|
bashkit/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bashkit
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Classifier: Development Status :: 4 - Beta
|
|
5
|
+
Classifier: Intended Audience :: Developers
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Rust
|
|
14
|
+
Classifier: Topic :: Software Development :: Interpreters
|
|
15
|
+
Classifier: Topic :: Security
|
|
16
|
+
Requires-Dist: deepagents>=0.3.11 ; extra == 'deepagents'
|
|
17
|
+
Requires-Dist: langchain-anthropic>=0.3 ; extra == 'deepagents'
|
|
18
|
+
Requires-Dist: pytest>=7.0 ; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest-asyncio>=0.23 ; extra == 'dev'
|
|
20
|
+
Requires-Dist: langchain-core>=0.3 ; extra == 'langchain'
|
|
21
|
+
Requires-Dist: langchain-anthropic>=0.3 ; extra == 'langchain'
|
|
22
|
+
Provides-Extra: deepagents
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Provides-Extra: langchain
|
|
25
|
+
Summary: Python bindings for Bashkit - a sandboxed bash interpreter for AI agents
|
|
26
|
+
Keywords: bash,sandbox,ai,agent,shell,interpreter
|
|
27
|
+
License: MIT
|
|
28
|
+
Requires-Python: >=3.9
|
|
29
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
30
|
+
|
|
31
|
+
# Bashkit Python Bindings
|
|
32
|
+
|
|
33
|
+
Python bindings for [Bashkit](https://github.com/everruns/bashkit) - a sandboxed bash interpreter for AI agents.
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- **Sandboxed execution**: All commands run in isolation with a virtual filesystem
|
|
38
|
+
- **68+ built-in commands**: echo, cat, grep, sed, awk, jq, curl, find, and more
|
|
39
|
+
- **Full bash syntax**: Variables, pipelines, redirects, loops, functions, arrays
|
|
40
|
+
- **Resource limits**: Protect against infinite loops and runaway scripts
|
|
41
|
+
- **LangChain integration**: Ready-to-use tool for LangChain agents
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# From PyPI (when published)
|
|
47
|
+
pip install bashkit
|
|
48
|
+
|
|
49
|
+
# With LangChain support
|
|
50
|
+
pip install 'bashkit[langchain]'
|
|
51
|
+
|
|
52
|
+
# From source
|
|
53
|
+
pip install maturin
|
|
54
|
+
maturin develop
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import asyncio
|
|
61
|
+
from bashkit import BashTool
|
|
62
|
+
|
|
63
|
+
async def main():
|
|
64
|
+
tool = BashTool()
|
|
65
|
+
|
|
66
|
+
# Simple command
|
|
67
|
+
result = await tool.execute("echo 'Hello, World!'")
|
|
68
|
+
print(result.stdout) # Hello, World!
|
|
69
|
+
|
|
70
|
+
# Pipeline
|
|
71
|
+
result = await tool.execute("echo -e 'banana\\napple\\ncherry' | sort")
|
|
72
|
+
print(result.stdout) # apple\nbanana\ncherry
|
|
73
|
+
|
|
74
|
+
# Virtual filesystem
|
|
75
|
+
result = await tool.execute("""
|
|
76
|
+
echo 'data' > /tmp/file.txt
|
|
77
|
+
cat /tmp/file.txt
|
|
78
|
+
""")
|
|
79
|
+
print(result.stdout) # data
|
|
80
|
+
|
|
81
|
+
asyncio.run(main())
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## LangChain Integration
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from bashkit.langchain import create_bash_tool
|
|
88
|
+
from langchain.agents import create_agent
|
|
89
|
+
|
|
90
|
+
# Create tool
|
|
91
|
+
bash_tool = create_bash_tool()
|
|
92
|
+
|
|
93
|
+
# Create agent
|
|
94
|
+
agent = create_agent(
|
|
95
|
+
model="claude-sonnet-4-20250514",
|
|
96
|
+
tools=[bash_tool],
|
|
97
|
+
system_prompt="You are a helpful assistant with bash skills."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Run
|
|
101
|
+
result = agent.invoke({
|
|
102
|
+
"messages": [{"role": "user", "content": "Create a file with today's date"}]
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Configuration
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
tool = BashTool(
|
|
110
|
+
username="agent", # Custom username (whoami)
|
|
111
|
+
hostname="sandbox", # Custom hostname
|
|
112
|
+
max_commands=1000, # Limit total commands
|
|
113
|
+
max_loop_iterations=10000, # Limit loop iterations
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Synchronous API
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from bashkit import BashTool
|
|
121
|
+
|
|
122
|
+
tool = BashTool()
|
|
123
|
+
result = tool.execute_sync("echo 'Hello!'")
|
|
124
|
+
print(result.stdout)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## API Reference
|
|
128
|
+
|
|
129
|
+
### BashTool
|
|
130
|
+
|
|
131
|
+
- `execute(commands: str) -> ExecResult`: Execute commands asynchronously
|
|
132
|
+
- `execute_sync(commands: str) -> ExecResult`: Execute commands synchronously
|
|
133
|
+
- `description() -> str`: Get tool description
|
|
134
|
+
- `help() -> str`: Get LLM documentation
|
|
135
|
+
- `input_schema() -> str`: Get JSON input schema
|
|
136
|
+
- `output_schema() -> str`: Get JSON output schema
|
|
137
|
+
|
|
138
|
+
### ExecResult
|
|
139
|
+
|
|
140
|
+
- `stdout: str`: Standard output
|
|
141
|
+
- `stderr: str`: Standard error
|
|
142
|
+
- `exit_code: int`: Exit code (0 = success)
|
|
143
|
+
- `error: Optional[str]`: Error message if execution failed
|
|
144
|
+
- `success: bool`: True if exit_code == 0
|
|
145
|
+
- `to_dict() -> dict`: Convert to dictionary
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT
|
|
150
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
bashkit\__init__.py,sha256=HROdnwXGjHVQ0y04XdXnrlIs7wPKyJrdgragm2yXU0Y,770
|
|
2
|
+
bashkit\_bashkit.cp39-win_amd64.pyd,sha256=gycW5oS4QTkcjqNXijtsuhrtT11R8S3hB9oFnQY5xQc,5978112
|
|
3
|
+
bashkit\_bashkit.pyi,sha256=wT_nNxhsaLWPSx6uoHEXF84j0uBpk-dMDu6Ffo07hu8,2740
|
|
4
|
+
bashkit\deepagents.py,sha256=mOOq40P1BAdfQohbdVpki_AkCOzPZMZjQpMzNQo2mTQ,13584
|
|
5
|
+
bashkit\langchain.py,sha256=F7xZVxK-qYFPRw4ebz9WuGUlwhHCEjMTPCNu_U3wheQ,5249
|
|
6
|
+
bashkit\py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
bashkit-0.1.4.dist-info\METADATA,sha256=Re1qoMU9C2iRDeflX4ZaIj0WnoE0qJYuugltFS585Dw,4227
|
|
8
|
+
bashkit-0.1.4.dist-info\WHEEL,sha256=H5klTgXu3iVXpFbMzUkXja9m3gL244ExCR0k1sRMImo,95
|
|
9
|
+
bashkit-0.1.4.dist-info\RECORD,,
|