cocoonstack-sandbox-langchain 0.1.0__tar.gz
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.
- cocoonstack_sandbox_langchain-0.1.0/PKG-INFO +33 -0
- cocoonstack_sandbox_langchain-0.1.0/README.md +23 -0
- cocoonstack_sandbox_langchain-0.1.0/cocoonsandbox_langchain/__init__.py +7 -0
- cocoonstack_sandbox_langchain-0.1.0/cocoonsandbox_langchain/toolkit.py +127 -0
- cocoonstack_sandbox_langchain-0.1.0/cocoonstack_sandbox_langchain.egg-info/PKG-INFO +33 -0
- cocoonstack_sandbox_langchain-0.1.0/cocoonstack_sandbox_langchain.egg-info/SOURCES.txt +10 -0
- cocoonstack_sandbox_langchain-0.1.0/cocoonstack_sandbox_langchain.egg-info/dependency_links.txt +1 -0
- cocoonstack_sandbox_langchain-0.1.0/cocoonstack_sandbox_langchain.egg-info/requires.txt +2 -0
- cocoonstack_sandbox_langchain-0.1.0/cocoonstack_sandbox_langchain.egg-info/top_level.txt +1 -0
- cocoonstack_sandbox_langchain-0.1.0/pyproject.toml +22 -0
- cocoonstack_sandbox_langchain-0.1.0/setup.cfg +4 -0
- cocoonstack_sandbox_langchain-0.1.0/tests/test_toolkit.py +88 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cocoonstack-sandbox-langchain
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LangChain tools backed by cocoon microVM sandboxes
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: langchain-core>=1.0
|
|
9
|
+
Requires-Dist: cocoonstack-sandbox>=0.1
|
|
10
|
+
|
|
11
|
+
# cocoonstack-sandbox-langchain
|
|
12
|
+
|
|
13
|
+
LangChain tools backed by cocoon microVM sandboxes: a `CocoonToolkit`
|
|
14
|
+
claims one sandbox lazily on first tool use and exposes it as
|
|
15
|
+
`StructuredTool`s an agent can call — shell exec, file read/write, and
|
|
16
|
+
directory listing, with disk state persisting across calls.
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from cocoonsandbox_langchain import CocoonToolkit
|
|
20
|
+
|
|
21
|
+
with CocoonToolkit("10.0.0.5:7777", api_token="...") as kit:
|
|
22
|
+
agent = create_react_agent(model, kit.get_tools())
|
|
23
|
+
agent.invoke({"messages": [("user", "run the tests in /work")]})
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`from_checkpoint="ck_..."` branches the sandbox from a checkpoint's
|
|
27
|
+
captured moment instead of a clean template — agents resume from prepared
|
|
28
|
+
state (dependencies installed, repo cloned) in milliseconds.
|
|
29
|
+
|
|
30
|
+
Sync-native (`_run` calls the stdlib-only cocoonstack-sandbox SDK
|
|
31
|
+
directly); async agents get `_arun` bridged via `asyncio.to_thread`.
|
|
32
|
+
|
|
33
|
+
Full reference: https://cocoonstack.github.io/sandbox/langchain
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# cocoonstack-sandbox-langchain
|
|
2
|
+
|
|
3
|
+
LangChain tools backed by cocoon microVM sandboxes: a `CocoonToolkit`
|
|
4
|
+
claims one sandbox lazily on first tool use and exposes it as
|
|
5
|
+
`StructuredTool`s an agent can call — shell exec, file read/write, and
|
|
6
|
+
directory listing, with disk state persisting across calls.
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
from cocoonsandbox_langchain import CocoonToolkit
|
|
10
|
+
|
|
11
|
+
with CocoonToolkit("10.0.0.5:7777", api_token="...") as kit:
|
|
12
|
+
agent = create_react_agent(model, kit.get_tools())
|
|
13
|
+
agent.invoke({"messages": [("user", "run the tests in /work")]})
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`from_checkpoint="ck_..."` branches the sandbox from a checkpoint's
|
|
17
|
+
captured moment instead of a clean template — agents resume from prepared
|
|
18
|
+
state (dependencies installed, repo cloned) in milliseconds.
|
|
19
|
+
|
|
20
|
+
Sync-native (`_run` calls the stdlib-only cocoonstack-sandbox SDK
|
|
21
|
+
directly); async agents get `_arun` bridged via `asyncio.to_thread`.
|
|
22
|
+
|
|
23
|
+
Full reference: https://cocoonstack.github.io/sandbox/langchain
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""LangChain toolkit over cocoon microVM sandboxes: one claimed sandbox
|
|
2
|
+
behind a set of StructuredTools (exec, file read/write, list). The claim is
|
|
3
|
+
lazy — building an agent costs nothing until a tool actually runs — and the
|
|
4
|
+
toolkit is a context manager whose exit releases the sandbox."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
import threading
|
|
11
|
+
|
|
12
|
+
from cocoonsandbox import Client, Sandbox
|
|
13
|
+
from langchain_core.tools import StructuredTool
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ExecInput(BaseModel):
|
|
18
|
+
command: str = Field(description="shell command, run via sh -c inside the sandbox")
|
|
19
|
+
cwd: str = Field(default="", description="working directory (optional)")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WriteFileInput(BaseModel):
|
|
23
|
+
path: str = Field(description="absolute path in the sandbox")
|
|
24
|
+
content: str = Field(description="file content to write")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PathInput(BaseModel):
|
|
28
|
+
path: str = Field(description="absolute path in the sandbox")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CocoonToolkit:
|
|
32
|
+
"""LangChain tools backed by one sandbox, claimed on first tool use.
|
|
33
|
+
|
|
34
|
+
from_checkpoint branches a fresh sandbox from that checkpoint's captured
|
|
35
|
+
moment instead of claiming a clean template — agents resume from
|
|
36
|
+
prepared state.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, addr: str, api_token: str = "", template: str = "rt:24.04",
|
|
40
|
+
net: str = "", ttl_seconds: int = 0, from_checkpoint: str = ""):
|
|
41
|
+
self._client = Client(addr, api_token=api_token)
|
|
42
|
+
self._template = template
|
|
43
|
+
self._net = net
|
|
44
|
+
self._ttl = ttl_seconds
|
|
45
|
+
self._from_checkpoint = from_checkpoint
|
|
46
|
+
self._lock = threading.Lock()
|
|
47
|
+
self._sb: Sandbox | None = None
|
|
48
|
+
self._closed = False
|
|
49
|
+
|
|
50
|
+
def __enter__(self) -> CocoonToolkit:
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
def __exit__(self, *exc) -> None:
|
|
54
|
+
self.close()
|
|
55
|
+
|
|
56
|
+
def get_tools(self) -> list[StructuredTool]:
|
|
57
|
+
"""The sandbox tool set; sync-native (_run), async via to_thread."""
|
|
58
|
+
return [
|
|
59
|
+
self._tool("sandbox_exec",
|
|
60
|
+
"Run a shell command in the sandbox; returns stdout, stderr, "
|
|
61
|
+
"and the exit code. State on disk persists across calls.",
|
|
62
|
+
ExecInput, self._exec),
|
|
63
|
+
self._tool("sandbox_write_file",
|
|
64
|
+
"Write a text file in the sandbox (atomic).",
|
|
65
|
+
WriteFileInput, self._write_file),
|
|
66
|
+
self._tool("sandbox_read_file",
|
|
67
|
+
"Read a text file from the sandbox.",
|
|
68
|
+
PathInput, self._read_file),
|
|
69
|
+
self._tool("sandbox_list_dir",
|
|
70
|
+
"List a sandbox directory as JSON entries.",
|
|
71
|
+
PathInput, self._list_dir),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
def close(self) -> None:
|
|
75
|
+
"""Releases the sandbox; safe to call twice or before any claim.
|
|
76
|
+
Tools invoked after close raise instead of silently claiming a
|
|
77
|
+
sandbox nothing would ever release."""
|
|
78
|
+
with self._lock:
|
|
79
|
+
sb, self._sb = self._sb, None
|
|
80
|
+
self._closed = True
|
|
81
|
+
if sb is not None:
|
|
82
|
+
sb.close()
|
|
83
|
+
|
|
84
|
+
def sandbox(self) -> Sandbox:
|
|
85
|
+
"""The claimed sandbox, claiming (or branching) on first use."""
|
|
86
|
+
with self._lock:
|
|
87
|
+
if self._closed:
|
|
88
|
+
raise RuntimeError("toolkit is closed")
|
|
89
|
+
if self._sb is None:
|
|
90
|
+
self._sb = self._claim()
|
|
91
|
+
return self._sb
|
|
92
|
+
|
|
93
|
+
def _claim(self) -> Sandbox:
|
|
94
|
+
if self._from_checkpoint:
|
|
95
|
+
return self._client.checkpoint(self._from_checkpoint).new(ttl_seconds=self._ttl)
|
|
96
|
+
return self._client.new(self._template, net=self._net, ttl_seconds=self._ttl)
|
|
97
|
+
|
|
98
|
+
def _tool(self, name: str, description: str, schema: type[BaseModel], func) -> StructuredTool:
|
|
99
|
+
async def arun(**kwargs):
|
|
100
|
+
return await asyncio.to_thread(func, **kwargs)
|
|
101
|
+
|
|
102
|
+
return StructuredTool.from_function(func=func, coroutine=arun, name=name,
|
|
103
|
+
description=description, args_schema=schema)
|
|
104
|
+
|
|
105
|
+
def _exec(self, command: str, cwd: str = "") -> str:
|
|
106
|
+
out: list[bytes] = []
|
|
107
|
+
errs: list[bytes] = []
|
|
108
|
+
code = self.sandbox().run(["sh", "-c", command], cwd=cwd,
|
|
109
|
+
on_stdout=out.append, on_stderr=errs.append)
|
|
110
|
+
stdout = b"".join(out).decode(errors="replace")
|
|
111
|
+
stderr = b"".join(errs).decode(errors="replace")
|
|
112
|
+
result = stdout
|
|
113
|
+
if stderr:
|
|
114
|
+
result += ("\n" if result else "") + "stderr: " + stderr
|
|
115
|
+
if code != 0:
|
|
116
|
+
result += ("\n" if result else "") + f"exit code: {code}"
|
|
117
|
+
return result or "(no output)"
|
|
118
|
+
|
|
119
|
+
def _write_file(self, path: str, content: str) -> str:
|
|
120
|
+
self.sandbox().write_file(path, content.encode())
|
|
121
|
+
return f"wrote {path}"
|
|
122
|
+
|
|
123
|
+
def _read_file(self, path: str) -> str:
|
|
124
|
+
return self.sandbox().read_file(path).decode(errors="replace")
|
|
125
|
+
|
|
126
|
+
def _list_dir(self, path: str) -> str:
|
|
127
|
+
return json.dumps(self.sandbox().list_dir(path))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cocoonstack-sandbox-langchain
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LangChain tools backed by cocoon microVM sandboxes
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: langchain-core>=1.0
|
|
9
|
+
Requires-Dist: cocoonstack-sandbox>=0.1
|
|
10
|
+
|
|
11
|
+
# cocoonstack-sandbox-langchain
|
|
12
|
+
|
|
13
|
+
LangChain tools backed by cocoon microVM sandboxes: a `CocoonToolkit`
|
|
14
|
+
claims one sandbox lazily on first tool use and exposes it as
|
|
15
|
+
`StructuredTool`s an agent can call — shell exec, file read/write, and
|
|
16
|
+
directory listing, with disk state persisting across calls.
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from cocoonsandbox_langchain import CocoonToolkit
|
|
20
|
+
|
|
21
|
+
with CocoonToolkit("10.0.0.5:7777", api_token="...") as kit:
|
|
22
|
+
agent = create_react_agent(model, kit.get_tools())
|
|
23
|
+
agent.invoke({"messages": [("user", "run the tests in /work")]})
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`from_checkpoint="ck_..."` branches the sandbox from a checkpoint's
|
|
27
|
+
captured moment instead of a clean template — agents resume from prepared
|
|
28
|
+
state (dependencies installed, repo cloned) in milliseconds.
|
|
29
|
+
|
|
30
|
+
Sync-native (`_run` calls the stdlib-only cocoonstack-sandbox SDK
|
|
31
|
+
directly); async agents get `_arun` bridged via `asyncio.to_thread`.
|
|
32
|
+
|
|
33
|
+
Full reference: https://cocoonstack.github.io/sandbox/langchain
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
cocoonsandbox_langchain/__init__.py
|
|
4
|
+
cocoonsandbox_langchain/toolkit.py
|
|
5
|
+
cocoonstack_sandbox_langchain.egg-info/PKG-INFO
|
|
6
|
+
cocoonstack_sandbox_langchain.egg-info/SOURCES.txt
|
|
7
|
+
cocoonstack_sandbox_langchain.egg-info/dependency_links.txt
|
|
8
|
+
cocoonstack_sandbox_langchain.egg-info/requires.txt
|
|
9
|
+
cocoonstack_sandbox_langchain.egg-info/top_level.txt
|
|
10
|
+
tests/test_toolkit.py
|
cocoonstack_sandbox_langchain-0.1.0/cocoonstack_sandbox_langchain.egg-info/dependency_links.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cocoonsandbox_langchain
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cocoonstack-sandbox-langchain"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "LangChain tools backed by cocoon microVM sandboxes"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
dependencies = ["langchain-core>=1.0", "cocoonstack-sandbox>=0.1"]
|
|
13
|
+
|
|
14
|
+
[tool.setuptools.packages.find]
|
|
15
|
+
include = ["cocoonsandbox_langchain*"]
|
|
16
|
+
|
|
17
|
+
[tool.ruff]
|
|
18
|
+
line-length = 120
|
|
19
|
+
target-version = "py310"
|
|
20
|
+
|
|
21
|
+
[tool.ruff.lint]
|
|
22
|
+
select = ["E", "F", "W", "I", "UP", "B", "SIM"]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Toolkit wiring against a fake sandbox: tool names/schemas, exec output
|
|
2
|
+
shaping, lazy claim, and double-close safety — no real node."""
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from cocoonsandbox_langchain import CocoonToolkit
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FakeSandbox:
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.closed = 0
|
|
14
|
+
self.files = {}
|
|
15
|
+
|
|
16
|
+
def run(self, argv, cwd="", on_stdout=None, on_stderr=None, **_):
|
|
17
|
+
assert argv[:2] == ["sh", "-c"]
|
|
18
|
+
if argv[2] == "boom":
|
|
19
|
+
on_stderr(b"kaboom\n")
|
|
20
|
+
return 3
|
|
21
|
+
on_stdout(f"ran: {argv[2]}\n".encode())
|
|
22
|
+
return 0
|
|
23
|
+
|
|
24
|
+
def write_file(self, path, data):
|
|
25
|
+
self.files[path] = data
|
|
26
|
+
|
|
27
|
+
def read_file(self, path):
|
|
28
|
+
return self.files[path]
|
|
29
|
+
|
|
30
|
+
def list_dir(self, path):
|
|
31
|
+
return [{"name": "a.txt", "kind": "file", "size": 3}]
|
|
32
|
+
|
|
33
|
+
def close(self):
|
|
34
|
+
self.closed += 1
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def hooked(monkeypatch):
|
|
38
|
+
kit = CocoonToolkit("127.0.0.1:1")
|
|
39
|
+
fake = FakeSandbox()
|
|
40
|
+
monkeypatch.setattr(kit, "_claim", lambda: fake)
|
|
41
|
+
return kit, fake
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_tools_shape(monkeypatch):
|
|
45
|
+
kit, _ = hooked(monkeypatch)
|
|
46
|
+
tools = kit.get_tools()
|
|
47
|
+
names = [t.name for t in tools]
|
|
48
|
+
assert names == ["sandbox_exec", "sandbox_write_file", "sandbox_read_file", "sandbox_list_dir"]
|
|
49
|
+
assert all(t.description for t in tools)
|
|
50
|
+
assert kit._sb is None, "get_tools must not claim"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_exec_tool_output(monkeypatch):
|
|
54
|
+
kit, _ = hooked(monkeypatch)
|
|
55
|
+
exec_tool = kit.get_tools()[0]
|
|
56
|
+
assert exec_tool.invoke({"command": "echo hi"}) == "ran: echo hi\n"
|
|
57
|
+
out = exec_tool.invoke({"command": "boom"})
|
|
58
|
+
assert "kaboom" in out and "exit code: 3" in out
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_async_bridge(monkeypatch):
|
|
62
|
+
kit, _ = hooked(monkeypatch)
|
|
63
|
+
exec_tool = kit.get_tools()[0]
|
|
64
|
+
result = asyncio.run(exec_tool.ainvoke({"command": "echo async"}))
|
|
65
|
+
assert result == "ran: echo async\n"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_file_tools_round_trip(monkeypatch):
|
|
69
|
+
kit, _ = hooked(monkeypatch)
|
|
70
|
+
_, write, read, list_dir = kit.get_tools()
|
|
71
|
+
assert write.invoke({"path": "/w/a.txt", "content": "body"}) == "wrote /w/a.txt"
|
|
72
|
+
assert read.invoke({"path": "/w/a.txt"}) == "body"
|
|
73
|
+
assert "a.txt" in list_dir.invoke({"path": "/w"})
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_close_releases_once(monkeypatch):
|
|
77
|
+
kit, fake = hooked(monkeypatch)
|
|
78
|
+
kit.get_tools()[0].invoke({"command": "x"}) # forces the claim
|
|
79
|
+
kit.close()
|
|
80
|
+
kit.close()
|
|
81
|
+
assert fake.closed == 1
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_use_after_close_raises(monkeypatch):
|
|
85
|
+
kit, _ = hooked(monkeypatch)
|
|
86
|
+
kit.close()
|
|
87
|
+
with pytest.raises(RuntimeError):
|
|
88
|
+
kit.sandbox()
|