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.
@@ -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,7 @@
1
+ """LangChain tools backed by cocoon microVM sandboxes."""
2
+
3
+ from .toolkit import CocoonToolkit
4
+
5
+ __all__ = [
6
+ "CocoonToolkit",
7
+ ]
@@ -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
@@ -0,0 +1,2 @@
1
+ langchain-core>=1.0
2
+ cocoonstack-sandbox>=0.1
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()