agentkernel-sdk 0.2.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,3 @@
1
+ .pytest_cache/
2
+ .venv/
3
+ __pycache__/
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentkernel-sdk
3
+ Version: 0.2.0
4
+ Summary: Python SDK for agentkernel — run AI coding agents in secure, isolated microVMs
5
+ Project-URL: Homepage, https://github.com/thrashr888/agentkernel
6
+ Project-URL: Repository, https://github.com/thrashr888/agentkernel/tree/main/sdk/python
7
+ Author-email: Paul Thrasher <thrashr888@gmail.com>
8
+ License-Expression: MIT
9
+ Keywords: agent,agentkernel,ai,firecracker,microvm,sandbox
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: httpx-sse>=0.4
22
+ Requires-Dist: httpx>=0.27
23
+ Requires-Dist: pydantic>=2.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: mypy>=1.13; extra == 'dev'
26
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
27
+ Requires-Dist: pytest-httpx>=0.34; extra == 'dev'
28
+ Requires-Dist: pytest>=8.0; extra == 'dev'
29
+ Requires-Dist: ruff>=0.8; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # agentkernel
33
+
34
+ Python SDK for [agentkernel](https://github.com/thrashr888/agentkernel) — run AI coding agents in secure, isolated microVMs.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install agentkernel-sdk
40
+ ```
41
+
42
+ Requires Python 3.10+.
43
+
44
+ ## Quick Start
45
+
46
+ ```python
47
+ from agentkernel import AgentKernel
48
+
49
+ with AgentKernel() as client:
50
+ result = client.run(["echo", "hello"])
51
+ print(result.output) # "hello\n"
52
+ ```
53
+
54
+ ## Async
55
+
56
+ ```python
57
+ from agentkernel import AsyncAgentKernel
58
+
59
+ async with AsyncAgentKernel() as client:
60
+ result = await client.run(["echo", "hello"])
61
+ print(result.output)
62
+ ```
63
+
64
+ ## Sandbox Sessions
65
+
66
+ ```python
67
+ with AgentKernel() as client:
68
+ with client.sandbox("test", image="python:3.12-alpine") as sb:
69
+ sb.run(["pip", "install", "numpy"])
70
+ result = sb.run(["python3", "-c", "import numpy; print(numpy.__version__)"])
71
+ print(result.output)
72
+ # sandbox auto-removed
73
+ ```
74
+
75
+ ## Streaming
76
+
77
+ ```python
78
+ for event in client.run_stream(["python3", "script.py"]):
79
+ if event.type == "output":
80
+ print(event.data["data"], end="")
81
+ ```
82
+
83
+ ## Configuration
84
+
85
+ ```python
86
+ client = AgentKernel(
87
+ base_url="http://localhost:18888", # default
88
+ api_key="sk-...", # optional
89
+ timeout=30.0, # default
90
+ )
91
+ ```
92
+
93
+ Or use environment variables:
94
+
95
+ ```bash
96
+ export AGENTKERNEL_BASE_URL=http://localhost:18888
97
+ export AGENTKERNEL_API_KEY=sk-...
98
+ ```
99
+
100
+ ## License
101
+
102
+ MIT
@@ -0,0 +1,71 @@
1
+ # agentkernel
2
+
3
+ Python SDK for [agentkernel](https://github.com/thrashr888/agentkernel) — run AI coding agents in secure, isolated microVMs.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install agentkernel-sdk
9
+ ```
10
+
11
+ Requires Python 3.10+.
12
+
13
+ ## Quick Start
14
+
15
+ ```python
16
+ from agentkernel import AgentKernel
17
+
18
+ with AgentKernel() as client:
19
+ result = client.run(["echo", "hello"])
20
+ print(result.output) # "hello\n"
21
+ ```
22
+
23
+ ## Async
24
+
25
+ ```python
26
+ from agentkernel import AsyncAgentKernel
27
+
28
+ async with AsyncAgentKernel() as client:
29
+ result = await client.run(["echo", "hello"])
30
+ print(result.output)
31
+ ```
32
+
33
+ ## Sandbox Sessions
34
+
35
+ ```python
36
+ with AgentKernel() as client:
37
+ with client.sandbox("test", image="python:3.12-alpine") as sb:
38
+ sb.run(["pip", "install", "numpy"])
39
+ result = sb.run(["python3", "-c", "import numpy; print(numpy.__version__)"])
40
+ print(result.output)
41
+ # sandbox auto-removed
42
+ ```
43
+
44
+ ## Streaming
45
+
46
+ ```python
47
+ for event in client.run_stream(["python3", "script.py"]):
48
+ if event.type == "output":
49
+ print(event.data["data"], end="")
50
+ ```
51
+
52
+ ## Configuration
53
+
54
+ ```python
55
+ client = AgentKernel(
56
+ base_url="http://localhost:18888", # default
57
+ api_key="sk-...", # optional
58
+ timeout=30.0, # default
59
+ )
60
+ ```
61
+
62
+ Or use environment variables:
63
+
64
+ ```bash
65
+ export AGENTKERNEL_BASE_URL=http://localhost:18888
66
+ export AGENTKERNEL_API_KEY=sk-...
67
+ ```
68
+
69
+ ## License
70
+
71
+ MIT
@@ -0,0 +1,31 @@
1
+ """Async example for agentkernel Python SDK."""
2
+
3
+ import asyncio
4
+ import sys
5
+
6
+ from agentkernel import AsyncAgentKernel
7
+
8
+
9
+ async def main() -> None:
10
+ async with AsyncAgentKernel() as client:
11
+ # Health check
12
+ print("Health:", await client.health())
13
+
14
+ # Run a command
15
+ result = await client.run(["echo", "Hello from async agentkernel!"])
16
+ print("Output:", result.output)
17
+
18
+ # Sandbox session with async context manager
19
+ async with client.sandbox("async-demo", image="python:3.12-alpine") as sb:
20
+ await sb.run(["pip", "install", "requests"])
21
+ result = await sb.run(["python3", "-c", "import requests; print(requests.__version__)"])
22
+ print(result.output)
23
+ # sandbox auto-removed
24
+
25
+ # Async streaming
26
+ async for event in client.run_stream(["echo", "streaming async"]):
27
+ if event.event_type == "output":
28
+ sys.stdout.write(event.data.get("data", ""))
29
+
30
+
31
+ asyncio.run(main())
@@ -0,0 +1,16 @@
1
+ """Quick start example for agentkernel Python SDK."""
2
+
3
+ from agentkernel import AgentKernel
4
+
5
+ client = AgentKernel()
6
+
7
+ # Health check
8
+ print("Health:", client.health())
9
+
10
+ # Run a command
11
+ result = client.run(["echo", "Hello from agentkernel!"])
12
+ print("Output:", result.output)
13
+
14
+ # List sandboxes
15
+ sandboxes = client.list_sandboxes()
16
+ print("Sandboxes:", sandboxes)
@@ -0,0 +1,16 @@
1
+ """Sandbox session example for agentkernel Python SDK."""
2
+
3
+ from agentkernel import AgentKernel
4
+
5
+ client = AgentKernel()
6
+
7
+ # Create a sandbox session — auto-removed when context manager exits
8
+ with client.sandbox("demo", image="python:3.12-alpine") as sb:
9
+ # Install a package
10
+ sb.run(["pip", "install", "numpy"])
11
+
12
+ # Run code
13
+ result = sb.run(["python3", "-c", "import numpy; print(f'numpy {numpy.__version__}')"])
14
+ print(result.output)
15
+
16
+ # Sandbox is automatically removed here
@@ -0,0 +1,19 @@
1
+ """Streaming example for agentkernel Python SDK."""
2
+
3
+ import sys
4
+
5
+ from agentkernel import AgentKernel
6
+
7
+ client = AgentKernel()
8
+
9
+ # Stream output from a command
10
+ for event in client.run_stream(["python3", "-c", "print('Hello from streaming!')"]):
11
+ match event.event_type:
12
+ case "started":
13
+ print("[started]", event.data)
14
+ case "output":
15
+ sys.stdout.write(event.data.get("data", ""))
16
+ case "done":
17
+ print(f"\n[done] exit_code: {event.data.get('exit_code')}")
18
+ case "error":
19
+ print(f"[error] {event.data.get('message')}", file=sys.stderr)
@@ -0,0 +1,57 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "agentkernel-sdk"
7
+ version = "0.2.0"
8
+ description = "Python SDK for agentkernel — run AI coding agents in secure, isolated microVMs"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "Paul Thrasher", email = "thrashr888@gmail.com" }]
13
+ keywords = ["agentkernel", "sandbox", "microvm", "ai", "agent", "firecracker"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Typing :: Typed",
25
+ ]
26
+ dependencies = ["httpx>=0.27", "httpx-sse>=0.4", "pydantic>=2.0"]
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "pytest>=8.0",
31
+ "pytest-asyncio>=0.24",
32
+ "pytest-httpx>=0.34",
33
+ "mypy>=1.13",
34
+ "ruff>=0.8",
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/thrashr888/agentkernel"
39
+ Repository = "https://github.com/thrashr888/agentkernel/tree/main/sdk/python"
40
+
41
+ [tool.hatch.build.targets.wheel]
42
+ packages = ["src/agentkernel"]
43
+
44
+ [tool.ruff]
45
+ target-version = "py310"
46
+ line-length = 100
47
+
48
+ [tool.ruff.lint]
49
+ select = ["E", "F", "I", "UP", "B"]
50
+
51
+ [tool.mypy]
52
+ python_version = "3.10"
53
+ strict = true
54
+
55
+ [tool.pytest.ini_options]
56
+ asyncio_mode = "auto"
57
+ markers = ["integration: requires running agentkernel server"]
@@ -0,0 +1,43 @@
1
+ """agentkernel SDK — run AI coding agents in secure, isolated microVMs."""
2
+
3
+ from .async_client import AsyncAgentKernel, AsyncSandboxSession
4
+ from .client import AgentKernel, SandboxSession
5
+ from .errors import (
6
+ AgentKernelError,
7
+ AuthError,
8
+ NetworkError,
9
+ NotFoundError,
10
+ ServerError,
11
+ StreamError,
12
+ ValidationError,
13
+ )
14
+ from .types import (
15
+ CreateSandboxOptions,
16
+ RunOptions,
17
+ RunOutput,
18
+ SandboxInfo,
19
+ SecurityProfile,
20
+ StreamEvent,
21
+ StreamEventType,
22
+ )
23
+
24
+ __all__ = [
25
+ "AgentKernel",
26
+ "AsyncAgentKernel",
27
+ "SandboxSession",
28
+ "AsyncSandboxSession",
29
+ "AgentKernelError",
30
+ "AuthError",
31
+ "NotFoundError",
32
+ "ValidationError",
33
+ "ServerError",
34
+ "NetworkError",
35
+ "StreamError",
36
+ "RunOutput",
37
+ "SandboxInfo",
38
+ "RunOptions",
39
+ "CreateSandboxOptions",
40
+ "SecurityProfile",
41
+ "StreamEvent",
42
+ "StreamEventType",
43
+ ]
@@ -0,0 +1,29 @@
1
+ """Configuration resolution for the agentkernel SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+
8
+ DEFAULT_BASE_URL = "http://localhost:18888"
9
+ DEFAULT_TIMEOUT = 30.0
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class Config:
14
+ base_url: str
15
+ api_key: str | None
16
+ timeout: float
17
+
18
+
19
+ def resolve_config(
20
+ base_url: str | None = None,
21
+ api_key: str | None = None,
22
+ timeout: float | None = None,
23
+ ) -> Config:
24
+ """Resolve config from constructor args > env vars > defaults."""
25
+ return Config(
26
+ base_url=(base_url or os.environ.get("AGENTKERNEL_BASE_URL") or DEFAULT_BASE_URL).rstrip("/"),
27
+ api_key=api_key or os.environ.get("AGENTKERNEL_API_KEY"),
28
+ timeout=timeout if timeout is not None else DEFAULT_TIMEOUT,
29
+ )
@@ -0,0 +1,199 @@
1
+ """Asynchronous client for the agentkernel HTTP API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import AsyncIterator
6
+ from types import TracebackType
7
+ from typing import Any
8
+
9
+ import httpx
10
+
11
+ from ._config import resolve_config
12
+ from .errors import AgentKernelError, NetworkError, error_from_status
13
+ from .types import (
14
+ CreateSandboxOptions,
15
+ RunOptions,
16
+ RunOutput,
17
+ SandboxInfo,
18
+ SecurityProfile,
19
+ StreamEvent,
20
+ )
21
+
22
+ SDK_VERSION = "0.1.0"
23
+
24
+
25
+ class AsyncSandboxSession:
26
+ """An async sandbox session with auto-cleanup on context manager exit."""
27
+
28
+ def __init__(self, name: str, client: AsyncAgentKernel) -> None:
29
+ self.name = name
30
+ self._client = client
31
+ self._removed = False
32
+
33
+ async def run(self, command: list[str]) -> RunOutput:
34
+ """Run a command in this sandbox."""
35
+ return await self._client.exec_in_sandbox(self.name, command)
36
+
37
+ async def info(self) -> SandboxInfo:
38
+ """Get sandbox info."""
39
+ return await self._client.get_sandbox(self.name)
40
+
41
+ async def remove(self) -> None:
42
+ """Remove the sandbox. Idempotent."""
43
+ if self._removed:
44
+ return
45
+ self._removed = True
46
+ await self._client.remove_sandbox(self.name)
47
+
48
+ async def __aenter__(self) -> AsyncSandboxSession:
49
+ return self
50
+
51
+ async def __aexit__(
52
+ self,
53
+ exc_type: type[BaseException] | None,
54
+ exc_val: BaseException | None,
55
+ exc_tb: TracebackType | None,
56
+ ) -> None:
57
+ await self.remove()
58
+
59
+
60
+ class AsyncAgentKernel:
61
+ """Asynchronous client for the agentkernel HTTP API.
62
+
63
+ Example::
64
+
65
+ async with AsyncAgentKernel() as client:
66
+ result = await client.run(["echo", "hello"])
67
+ print(result.output)
68
+ """
69
+
70
+ def __init__(
71
+ self,
72
+ base_url: str | None = None,
73
+ api_key: str | None = None,
74
+ timeout: float | None = None,
75
+ ) -> None:
76
+ config = resolve_config(base_url, api_key, timeout)
77
+ headers: dict[str, str] = {"User-Agent": f"agentkernel-python-sdk/{SDK_VERSION}"}
78
+ if config.api_key:
79
+ headers["Authorization"] = f"Bearer {config.api_key}"
80
+ self._http = httpx.AsyncClient(
81
+ base_url=config.base_url,
82
+ headers=headers,
83
+ timeout=config.timeout,
84
+ )
85
+
86
+ async def close(self) -> None:
87
+ """Close the HTTP client."""
88
+ await self._http.aclose()
89
+
90
+ async def __aenter__(self) -> AsyncAgentKernel:
91
+ return self
92
+
93
+ async def __aexit__(
94
+ self,
95
+ exc_type: type[BaseException] | None,
96
+ exc_val: BaseException | None,
97
+ exc_tb: TracebackType | None,
98
+ ) -> None:
99
+ await self.close()
100
+
101
+ # -- API methods --
102
+
103
+ async def health(self) -> str:
104
+ """Health check. Returns 'ok'."""
105
+ return await self._request("GET", "/health")
106
+
107
+ async def run(
108
+ self,
109
+ command: list[str],
110
+ *,
111
+ image: str | None = None,
112
+ profile: SecurityProfile | None = None,
113
+ fast: bool = True,
114
+ ) -> RunOutput:
115
+ """Run a command in a temporary sandbox."""
116
+ data = await self._request(
117
+ "POST",
118
+ "/run",
119
+ json={"command": command, "image": image, "profile": profile, "fast": fast},
120
+ )
121
+ return RunOutput(**data)
122
+
123
+ async def run_stream(
124
+ self,
125
+ command: list[str],
126
+ *,
127
+ image: str | None = None,
128
+ profile: SecurityProfile | None = None,
129
+ fast: bool = True,
130
+ ) -> AsyncIterator[StreamEvent]:
131
+ """Run a command with SSE streaming output."""
132
+ from .sse import iter_sse_async
133
+
134
+ response = await self._http.send(
135
+ self._http.build_request(
136
+ "POST",
137
+ "/run/stream",
138
+ json={"command": command, "image": image, "profile": profile, "fast": fast},
139
+ ),
140
+ stream=True,
141
+ )
142
+ if response.status_code >= 400:
143
+ await response.aread()
144
+ raise error_from_status(response.status_code, response.text)
145
+ return iter_sse_async(response)
146
+
147
+ async def list_sandboxes(self) -> list[SandboxInfo]:
148
+ """List all sandboxes."""
149
+ data = await self._request("GET", "/sandboxes")
150
+ return [SandboxInfo(**s) for s in data]
151
+
152
+ async def create_sandbox(self, name: str, *, image: str | None = None) -> SandboxInfo:
153
+ """Create a new sandbox."""
154
+ data = await self._request("POST", "/sandboxes", json={"name": name, "image": image})
155
+ return SandboxInfo(**data)
156
+
157
+ async def get_sandbox(self, name: str) -> SandboxInfo:
158
+ """Get info about a sandbox."""
159
+ data = await self._request("GET", f"/sandboxes/{name}")
160
+ return SandboxInfo(**data)
161
+
162
+ async def remove_sandbox(self, name: str) -> None:
163
+ """Remove a sandbox."""
164
+ await self._request("DELETE", f"/sandboxes/{name}")
165
+
166
+ async def exec_in_sandbox(self, name: str, command: list[str]) -> RunOutput:
167
+ """Run a command in an existing sandbox."""
168
+ data = await self._request("POST", f"/sandboxes/{name}/exec", json={"command": command})
169
+ return RunOutput(**data)
170
+
171
+ async def sandbox(self, name: str, *, image: str | None = None) -> AsyncSandboxSession:
172
+ """Create a sandbox session with automatic cleanup.
173
+
174
+ Example::
175
+
176
+ async with await client.sandbox("test") as sb:
177
+ await sb.run(["echo", "hello"])
178
+ # sandbox auto-removed
179
+ """
180
+ await self.create_sandbox(name, image=image)
181
+ return AsyncSandboxSession(name, self)
182
+
183
+ # -- Internal --
184
+
185
+ async def _request(self, method: str, path: str, **kwargs: Any) -> Any:
186
+ try:
187
+ response = await self._http.request(method, path, **kwargs)
188
+ except httpx.ConnectError as e:
189
+ raise NetworkError(f"Failed to connect: {e}") from e
190
+ except httpx.TimeoutException as e:
191
+ raise NetworkError(f"Request timed out: {e}") from e
192
+
193
+ if response.status_code >= 400:
194
+ raise error_from_status(response.status_code, response.text)
195
+
196
+ data = response.json()
197
+ if not data.get("success"):
198
+ raise AgentKernelError(data.get("error", "Unknown error"))
199
+ return data.get("data")
@@ -0,0 +1,185 @@
1
+ """Synchronous client for the agentkernel HTTP API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+ from typing import Any
7
+
8
+ import httpx
9
+
10
+ from ._config import resolve_config
11
+ from .errors import AgentKernelError, NetworkError, error_from_status
12
+ from .types import (
13
+ CreateSandboxOptions,
14
+ RunOptions,
15
+ RunOutput,
16
+ SandboxInfo,
17
+ SecurityProfile,
18
+ StreamEvent,
19
+ )
20
+
21
+ SDK_VERSION = "0.1.0"
22
+
23
+
24
+ class SandboxSession:
25
+ """A sandbox session with auto-cleanup on context manager exit."""
26
+
27
+ def __init__(self, name: str, client: AgentKernel) -> None:
28
+ self.name = name
29
+ self._client = client
30
+ self._removed = False
31
+
32
+ def run(self, command: list[str]) -> RunOutput:
33
+ """Run a command in this sandbox."""
34
+ return self._client.exec_in_sandbox(self.name, command)
35
+
36
+ def info(self) -> SandboxInfo:
37
+ """Get sandbox info."""
38
+ return self._client.get_sandbox(self.name)
39
+
40
+ def remove(self) -> None:
41
+ """Remove the sandbox. Idempotent."""
42
+ if self._removed:
43
+ return
44
+ self._removed = True
45
+ self._client.remove_sandbox(self.name)
46
+
47
+ def __enter__(self) -> SandboxSession:
48
+ return self
49
+
50
+ def __exit__(self, *args: Any) -> None:
51
+ self.remove()
52
+
53
+
54
+ class AgentKernel:
55
+ """Synchronous client for the agentkernel HTTP API.
56
+
57
+ Example::
58
+
59
+ with AgentKernel() as client:
60
+ result = client.run(["echo", "hello"])
61
+ print(result.output)
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ base_url: str | None = None,
67
+ api_key: str | None = None,
68
+ timeout: float | None = None,
69
+ ) -> None:
70
+ config = resolve_config(base_url, api_key, timeout)
71
+ headers: dict[str, str] = {"User-Agent": f"agentkernel-python-sdk/{SDK_VERSION}"}
72
+ if config.api_key:
73
+ headers["Authorization"] = f"Bearer {config.api_key}"
74
+ self._http = httpx.Client(
75
+ base_url=config.base_url,
76
+ headers=headers,
77
+ timeout=config.timeout,
78
+ )
79
+
80
+ def close(self) -> None:
81
+ """Close the HTTP client."""
82
+ self._http.close()
83
+
84
+ def __enter__(self) -> AgentKernel:
85
+ return self
86
+
87
+ def __exit__(self, *args: Any) -> None:
88
+ self.close()
89
+
90
+ # -- API methods --
91
+
92
+ def health(self) -> str:
93
+ """Health check. Returns 'ok'."""
94
+ return self._request("GET", "/health")
95
+
96
+ def run(
97
+ self,
98
+ command: list[str],
99
+ *,
100
+ image: str | None = None,
101
+ profile: SecurityProfile | None = None,
102
+ fast: bool = True,
103
+ ) -> RunOutput:
104
+ """Run a command in a temporary sandbox."""
105
+ data = self._request(
106
+ "POST",
107
+ "/run",
108
+ json={"command": command, "image": image, "profile": profile, "fast": fast},
109
+ )
110
+ return RunOutput(**data)
111
+
112
+ def run_stream(
113
+ self,
114
+ command: list[str],
115
+ *,
116
+ image: str | None = None,
117
+ profile: SecurityProfile | None = None,
118
+ fast: bool = True,
119
+ ) -> Iterator[StreamEvent]:
120
+ """Run a command with SSE streaming output."""
121
+ from .sse import iter_sse_sync
122
+
123
+ with self._http.stream(
124
+ "POST",
125
+ "/run/stream",
126
+ json={"command": command, "image": image, "profile": profile, "fast": fast},
127
+ ) as response:
128
+ if response.status_code >= 400:
129
+ response.read()
130
+ raise error_from_status(response.status_code, response.text)
131
+ yield from iter_sse_sync(response)
132
+
133
+ def list_sandboxes(self) -> list[SandboxInfo]:
134
+ """List all sandboxes."""
135
+ data = self._request("GET", "/sandboxes")
136
+ return [SandboxInfo(**s) for s in data]
137
+
138
+ def create_sandbox(self, name: str, *, image: str | None = None) -> SandboxInfo:
139
+ """Create a new sandbox."""
140
+ data = self._request("POST", "/sandboxes", json={"name": name, "image": image})
141
+ return SandboxInfo(**data)
142
+
143
+ def get_sandbox(self, name: str) -> SandboxInfo:
144
+ """Get info about a sandbox."""
145
+ data = self._request("GET", f"/sandboxes/{name}")
146
+ return SandboxInfo(**data)
147
+
148
+ def remove_sandbox(self, name: str) -> None:
149
+ """Remove a sandbox."""
150
+ self._request("DELETE", f"/sandboxes/{name}")
151
+
152
+ def exec_in_sandbox(self, name: str, command: list[str]) -> RunOutput:
153
+ """Run a command in an existing sandbox."""
154
+ data = self._request("POST", f"/sandboxes/{name}/exec", json={"command": command})
155
+ return RunOutput(**data)
156
+
157
+ def sandbox(self, name: str, *, image: str | None = None) -> SandboxSession:
158
+ """Create a sandbox session with automatic cleanup.
159
+
160
+ Example::
161
+
162
+ with client.sandbox("test", image="python:3.12-alpine") as sb:
163
+ sb.run(["pip", "install", "numpy"])
164
+ # sandbox auto-removed
165
+ """
166
+ self.create_sandbox(name, image=image)
167
+ return SandboxSession(name, self)
168
+
169
+ # -- Internal --
170
+
171
+ def _request(self, method: str, path: str, **kwargs: Any) -> Any:
172
+ try:
173
+ response = self._http.request(method, path, **kwargs)
174
+ except httpx.ConnectError as e:
175
+ raise NetworkError(f"Failed to connect: {e}") from e
176
+ except httpx.TimeoutException as e:
177
+ raise NetworkError(f"Request timed out: {e}") from e
178
+
179
+ if response.status_code >= 400:
180
+ raise error_from_status(response.status_code, response.text)
181
+
182
+ data = response.json()
183
+ if not data.get("success"):
184
+ raise AgentKernelError(data.get("error", "Unknown error"))
185
+ return data.get("data")
@@ -0,0 +1,54 @@
1
+ """Exception hierarchy for the agentkernel SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class AgentKernelError(Exception):
7
+ """Base error for all agentkernel SDK errors."""
8
+
9
+
10
+ class AuthError(AgentKernelError):
11
+ """401 Unauthorized."""
12
+
13
+ status = 401
14
+
15
+
16
+ class NotFoundError(AgentKernelError):
17
+ """404 Not Found."""
18
+
19
+ status = 404
20
+
21
+
22
+ class ValidationError(AgentKernelError):
23
+ """400 Bad Request."""
24
+
25
+ status = 400
26
+
27
+
28
+ class ServerError(AgentKernelError):
29
+ """500 Internal Server Error."""
30
+
31
+ status = 500
32
+
33
+
34
+ class NetworkError(AgentKernelError):
35
+ """Network / connection error."""
36
+
37
+
38
+ class StreamError(AgentKernelError):
39
+ """SSE streaming error."""
40
+
41
+
42
+ def error_from_status(status: int, body: str) -> AgentKernelError:
43
+ """Map an HTTP status code + body to the appropriate error."""
44
+ import json
45
+
46
+ try:
47
+ parsed = json.loads(body)
48
+ message = parsed.get("error", body)
49
+ except (json.JSONDecodeError, TypeError):
50
+ message = body
51
+
52
+ errors = {400: ValidationError, 401: AuthError, 404: NotFoundError}
53
+ cls = errors.get(status, ServerError)
54
+ return cls(message)
File without changes
@@ -0,0 +1,49 @@
1
+ """SSE stream parsing for the agentkernel SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import AsyncIterator, Iterator
7
+ from typing import TYPE_CHECKING
8
+
9
+ import httpx
10
+ import httpx_sse
11
+
12
+ from .types import StreamEvent
13
+
14
+ if TYPE_CHECKING:
15
+ pass
16
+
17
+ KNOWN_EVENTS = frozenset({"started", "progress", "output", "done", "error"})
18
+
19
+
20
+ def iter_sse_sync(response: httpx.Response) -> Iterator[StreamEvent]:
21
+ """Parse SSE events from a sync httpx response."""
22
+ with httpx_sse.connect_sse(response.stream) as event_source: # type: ignore[arg-type]
23
+ for sse in event_source.iter_sse():
24
+ if sse.event not in KNOWN_EVENTS:
25
+ continue
26
+ try:
27
+ data = json.loads(sse.data)
28
+ except (json.JSONDecodeError, TypeError):
29
+ data = {"raw": sse.data}
30
+ event = StreamEvent(type=sse.event, data=data) # type: ignore[arg-type]
31
+ yield event
32
+ if event.type in ("done", "error"):
33
+ return
34
+
35
+
36
+ async def iter_sse_async(response: httpx.Response) -> AsyncIterator[StreamEvent]:
37
+ """Parse SSE events from an async httpx response."""
38
+ async with httpx_sse.aconnect_sse(response.stream) as event_source: # type: ignore[arg-type]
39
+ async for sse in event_source.aiter_sse():
40
+ if sse.event not in KNOWN_EVENTS:
41
+ continue
42
+ try:
43
+ data = json.loads(sse.data)
44
+ except (json.JSONDecodeError, TypeError):
45
+ data = {"raw": sse.data}
46
+ event = StreamEvent(type=sse.event, data=data) # type: ignore[arg-type]
47
+ yield event
48
+ if event.type in ("done", "error"):
49
+ return
@@ -0,0 +1,47 @@
1
+ """Type definitions for the agentkernel SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from pydantic import BaseModel
8
+
9
+
10
+ SecurityProfile = Literal["permissive", "moderate", "restrictive"]
11
+ SandboxStatus = Literal["running", "stopped"]
12
+ StreamEventType = Literal["started", "progress", "output", "done", "error"]
13
+
14
+
15
+ class RunOutput(BaseModel):
16
+ """Output from a command execution."""
17
+
18
+ output: str
19
+
20
+
21
+ class SandboxInfo(BaseModel):
22
+ """Information about a sandbox."""
23
+
24
+ name: str
25
+ status: SandboxStatus
26
+ backend: str
27
+
28
+
29
+ class RunOptions(BaseModel):
30
+ """Options for the run command."""
31
+
32
+ image: str | None = None
33
+ profile: SecurityProfile | None = None
34
+ fast: bool = True
35
+
36
+
37
+ class CreateSandboxOptions(BaseModel):
38
+ """Options for creating a sandbox."""
39
+
40
+ image: str | None = None
41
+
42
+
43
+ class StreamEvent(BaseModel):
44
+ """SSE stream event."""
45
+
46
+ type: StreamEventType
47
+ data: dict
@@ -0,0 +1,64 @@
1
+ """Tests for the asynchronous AsyncAgentKernel client."""
2
+
3
+ import pytest
4
+ from pytest_httpx import HTTPXMock
5
+
6
+ from agentkernel import AsyncAgentKernel, NotFoundError, RunOutput, SandboxInfo
7
+
8
+ BASE_URL = "http://localhost:9999"
9
+
10
+
11
+ def make_client(**kwargs) -> AsyncAgentKernel:
12
+ return AsyncAgentKernel(base_url=BASE_URL, **kwargs)
13
+
14
+
15
+ class TestAsyncHealth:
16
+ async def test_returns_ok(self, httpx_mock: HTTPXMock) -> None:
17
+ httpx_mock.add_response(json={"success": True, "data": "ok"})
18
+ async with make_client() as client:
19
+ assert await client.health() == "ok"
20
+
21
+
22
+ class TestAsyncRun:
23
+ async def test_returns_output(self, httpx_mock: HTTPXMock) -> None:
24
+ httpx_mock.add_response(json={"success": True, "data": {"output": "hello\n"}})
25
+ async with make_client() as client:
26
+ result = await client.run(["echo", "hello"])
27
+ assert isinstance(result, RunOutput)
28
+ assert result.output == "hello\n"
29
+
30
+
31
+ class TestAsyncListSandboxes:
32
+ async def test_returns_list(self, httpx_mock: HTTPXMock) -> None:
33
+ httpx_mock.add_response(
34
+ json={
35
+ "success": True,
36
+ "data": [{"name": "sb-1", "status": "running", "backend": "docker"}],
37
+ }
38
+ )
39
+ async with make_client() as client:
40
+ result = await client.list_sandboxes()
41
+ assert len(result) == 1
42
+ assert isinstance(result[0], SandboxInfo)
43
+
44
+
45
+ class TestAsyncGetSandbox:
46
+ async def test_not_found(self, httpx_mock: HTTPXMock) -> None:
47
+ httpx_mock.add_response(status_code=404, json={"success": False, "error": "Not found"})
48
+ async with make_client() as client:
49
+ with pytest.raises(NotFoundError):
50
+ await client.get_sandbox("missing")
51
+
52
+
53
+ class TestAsyncSandboxSession:
54
+ async def test_auto_removes(self, httpx_mock: HTTPXMock) -> None:
55
+ httpx_mock.add_response(
56
+ json={"success": True, "data": {"name": "sess", "status": "running", "backend": "docker"}}
57
+ )
58
+ httpx_mock.add_response(json={"success": True, "data": "Sandbox removed"})
59
+
60
+ async with make_client() as client:
61
+ async with await client.sandbox("sess") as sb:
62
+ assert sb.name == "sess"
63
+ requests = httpx_mock.get_requests()
64
+ assert requests[-1].method == "DELETE"
@@ -0,0 +1,171 @@
1
+ """Tests for the synchronous AgentKernel client."""
2
+
3
+ import pytest
4
+ from pytest_httpx import HTTPXMock
5
+
6
+ from agentkernel import (
7
+ AgentKernel,
8
+ AuthError,
9
+ NotFoundError,
10
+ RunOutput,
11
+ SandboxInfo,
12
+ ServerError,
13
+ ValidationError,
14
+ )
15
+
16
+ BASE_URL = "http://localhost:9999"
17
+
18
+
19
+ def make_client(**kwargs) -> AgentKernel:
20
+ return AgentKernel(base_url=BASE_URL, **kwargs)
21
+
22
+
23
+ class TestHealth:
24
+ def test_returns_ok(self, httpx_mock: HTTPXMock) -> None:
25
+ httpx_mock.add_response(json={"success": True, "data": "ok"})
26
+ assert make_client().health() == "ok"
27
+
28
+
29
+ class TestRun:
30
+ def test_returns_output(self, httpx_mock: HTTPXMock) -> None:
31
+ httpx_mock.add_response(json={"success": True, "data": {"output": "hello\n"}})
32
+ result = make_client().run(["echo", "hello"])
33
+ assert isinstance(result, RunOutput)
34
+ assert result.output == "hello\n"
35
+
36
+ def test_passes_options(self, httpx_mock: HTTPXMock) -> None:
37
+ httpx_mock.add_response(json={"success": True, "data": {"output": "ok\n"}})
38
+ make_client().run(
39
+ ["python3", "-c", "print('ok')"],
40
+ image="python:3.12-alpine",
41
+ profile="restrictive",
42
+ fast=False,
43
+ )
44
+ request = httpx_mock.get_request()
45
+ assert request is not None
46
+ import json
47
+
48
+ body = json.loads(request.content)
49
+ assert body["image"] == "python:3.12-alpine"
50
+ assert body["profile"] == "restrictive"
51
+ assert body["fast"] is False
52
+
53
+
54
+ class TestListSandboxes:
55
+ def test_returns_list(self, httpx_mock: HTTPXMock) -> None:
56
+ httpx_mock.add_response(
57
+ json={
58
+ "success": True,
59
+ "data": [
60
+ {"name": "sb-1", "status": "running", "backend": "docker"},
61
+ {"name": "sb-2", "status": "stopped", "backend": "docker"},
62
+ ],
63
+ }
64
+ )
65
+ result = make_client().list_sandboxes()
66
+ assert len(result) == 2
67
+ assert all(isinstance(s, SandboxInfo) for s in result)
68
+ assert result[0].name == "sb-1"
69
+
70
+
71
+ class TestCreateSandbox:
72
+ def test_creates(self, httpx_mock: HTTPXMock) -> None:
73
+ httpx_mock.add_response(
74
+ status_code=201,
75
+ json={"success": True, "data": {"name": "new", "status": "running", "backend": "docker"}},
76
+ )
77
+ result = make_client().create_sandbox("new")
78
+ assert result.name == "new"
79
+ assert result.status == "running"
80
+
81
+
82
+ class TestGetSandbox:
83
+ def test_returns_info(self, httpx_mock: HTTPXMock) -> None:
84
+ httpx_mock.add_response(
85
+ json={"success": True, "data": {"name": "test", "status": "running", "backend": "docker"}}
86
+ )
87
+ result = make_client().get_sandbox("test")
88
+ assert result.name == "test"
89
+
90
+ def test_not_found(self, httpx_mock: HTTPXMock) -> None:
91
+ httpx_mock.add_response(status_code=404, json={"success": False, "error": "Not found"})
92
+ with pytest.raises(NotFoundError):
93
+ make_client().get_sandbox("missing")
94
+
95
+
96
+ class TestRemoveSandbox:
97
+ def test_removes(self, httpx_mock: HTTPXMock) -> None:
98
+ httpx_mock.add_response(json={"success": True, "data": "Sandbox removed"})
99
+ make_client().remove_sandbox("test") # no exception
100
+
101
+
102
+ class TestExecInSandbox:
103
+ def test_returns_output(self, httpx_mock: HTTPXMock) -> None:
104
+ httpx_mock.add_response(json={"success": True, "data": {"output": "result\n"}})
105
+ result = make_client().exec_in_sandbox("test", ["echo", "result"])
106
+ assert result.output == "result\n"
107
+
108
+
109
+ class TestSandboxSession:
110
+ def test_auto_removes(self, httpx_mock: HTTPXMock) -> None:
111
+ # create
112
+ httpx_mock.add_response(
113
+ json={"success": True, "data": {"name": "sess", "status": "running", "backend": "docker"}}
114
+ )
115
+ # remove
116
+ httpx_mock.add_response(json={"success": True, "data": "Sandbox removed"})
117
+
118
+ client = make_client()
119
+ with client.sandbox("sess") as sb:
120
+ assert sb.name == "sess"
121
+ # remove was called
122
+ requests = httpx_mock.get_requests()
123
+ assert requests[-1].method == "DELETE"
124
+
125
+ def test_remove_is_idempotent(self, httpx_mock: HTTPXMock) -> None:
126
+ httpx_mock.add_response(
127
+ json={"success": True, "data": {"name": "idem", "status": "running", "backend": "docker"}}
128
+ )
129
+ httpx_mock.add_response(json={"success": True, "data": "Sandbox removed"})
130
+
131
+ client = make_client()
132
+ sb = client.sandbox("idem")
133
+ sb.remove()
134
+ sb.remove() # second call is a no-op
135
+ delete_requests = [r for r in httpx_mock.get_requests() if r.method == "DELETE"]
136
+ assert len(delete_requests) == 1
137
+
138
+
139
+ class TestAuth:
140
+ def test_sends_bearer_token(self, httpx_mock: HTTPXMock) -> None:
141
+ httpx_mock.add_response(json={"success": True, "data": "ok"})
142
+ make_client(api_key="sk-test").health()
143
+ request = httpx_mock.get_request()
144
+ assert request is not None
145
+ assert request.headers["authorization"] == "Bearer sk-test"
146
+
147
+
148
+ class TestErrors:
149
+ def test_401(self, httpx_mock: HTTPXMock) -> None:
150
+ httpx_mock.add_response(status_code=401, json={"success": False, "error": "Unauthorized"})
151
+ with pytest.raises(AuthError):
152
+ make_client().health()
153
+
154
+ def test_400(self, httpx_mock: HTTPXMock) -> None:
155
+ httpx_mock.add_response(status_code=400, json={"success": False, "error": "Bad request"})
156
+ with pytest.raises(ValidationError):
157
+ make_client().run([])
158
+
159
+ def test_500(self, httpx_mock: HTTPXMock) -> None:
160
+ httpx_mock.add_response(status_code=500, json={"success": False, "error": "Internal error"})
161
+ with pytest.raises(ServerError):
162
+ make_client().health()
163
+
164
+
165
+ class TestUserAgent:
166
+ def test_sends_user_agent(self, httpx_mock: HTTPXMock) -> None:
167
+ httpx_mock.add_response(json={"success": True, "data": "ok"})
168
+ make_client().health()
169
+ request = httpx_mock.get_request()
170
+ assert request is not None
171
+ assert request.headers["user-agent"].startswith("agentkernel-python-sdk/")