Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ We aim for the Sandbox to be vendor-neutral, supporting various runtimes. Key ch

The current Roadmap can be found at [roadmap.md](roadmap.md).

## Security

For information on the security model, trust boundaries, and mitigations of Agent Sandbox, please refer to the [Threat Model](docs/security/threat_model.md).

## Community, Discussion, Contribution, and Support

This is a community-driven effort, and we welcome collaboration!
Expand Down
1 change: 1 addition & 0 deletions clients/integrations/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.egg-info/
89 changes: 89 additions & 0 deletions clients/integrations/deepagents/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# deepagents-k8s-agent-sandbox

LangChain DeepAgents backend for Kubernetes `agent-sandbox`. This package allows you to run `deepagents` in a secure, isolated Kubernetes sandbox environment instead of running locally.

## Quick Install

```bash
pip install deepagents-k8s-agent-sandbox
```

## Basic Usage

```python
from k8s_agent_sandbox.sandbox_client import SandboxClient
from deepagents_k8s_agent_sandbox import K8sAgentSandbox

client = SandboxClient()

sandbox = K8sAgentSandbox.from_existing_claim_name(
client,
claim_name="some-claim",
namespace="default",
# The directory the sandbox runtime's file API resolves relative paths
# against. This is a property of the sandbox image, not `root_dir`.
sandbox_api_cwd="/app",
)

result = sandbox.execute("echo hello")
print(result.output)
```

## Full Graph Example

Here is a full example of creating a `deepagents` agent graph that relies on the `K8sAgentSandbox` backend. In this example, the agent creates a python script, executes it securely in the sandbox, and returns the result.

```python
import os
from k8s_agent_sandbox import SandboxClient
from deepagents_k8s_agent_sandbox import K8sAgentSandbox
from deepagents_k8s_agent_sandbox.settings import K8sAgentSandboxSettings
from deepagents import create_deep_agent
from langchain_google_genai import ChatGoogleGenerativeAI

# 1. Setup the chat model (e.g., using LangChain with Gemini)
model = ChatGoogleGenerativeAI(model="gemini-3-pro")

# 2. Connect to the Kubernetes Sandbox cluster
client = SandboxClient()

# 3. Configure the sandbox settings
# 'warmpool' references a pre-defined SandboxWarmPool in your cluster
settings = K8sAgentSandboxSettings(
warmpool="python-deepagent",
namespace="default"
)

# 4. Initialize the sandbox backend
# This either creates a new sandbox claim or reuses an existing one
# that matches the "scope" labels for isolation.
backend = K8sAgentSandbox.from_labels_scope(
client=client,
sandbox_settings=settings,
scope={"thread": "my-graph-thread"},
# The directory the sandbox runtime's file API resolves relative paths
# against. This is a property of the sandbox image, not `root_dir`.
sandbox_api_cwd="/app",
)

# 5. Create the DeepAgent graph, injecting the K8s sandbox as the backend
agent = create_deep_agent(
model=model,
backend=backend,
# (Optional) pass the path to custom skills:
# skills=["./skills/python-scripting"]
)

# 6. Invoke the agent with a task that requires sandbox execution
query = "Write a python script that prints 'Hello from Sandbox', run it, and tell me the output."

print(f"User: {query}\n")
response = agent.invoke({
"messages": [("user", query)]
})

# 7. Print the results from the agent
for msg in response.get("messages", []):
if msg.type == "ai":
print(f"Agent: {msg.content}")
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright 2026 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from .sandbox import (
K8sAgentSandbox,
)

__all__ = [
"K8sAgentSandbox",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Copyright 2026 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from abc import abstractmethod, ABC
import logging

from k8s_agent_sandbox import SandboxClient, SandboxNotFoundError
from k8s_agent_sandbox.sandbox import Sandbox

from .settings import K8sAgentSandboxSettings


logger = logging.getLogger(__name__)


class K8sAgentSandboxLifecycleManager(ABC):
"""A helper class that takes care of managing the sandbox instance."""
def __init__(self) -> None:
self._sandbox = None

def get_sandbox(self) -> Sandbox:
if self._sandbox is not None:
return self._sandbox

self._sandbox = self._get_sandbox()
return self._sandbox

@abstractmethod
def _get_sandbox(self) -> Sandbox:
pass

class ExistingSandboxInstanceLifecycleManager(K8sAgentSandboxLifecycleManager):
"""Simple manager that uses existing sandbox instance without managing it."""
def __init__(self, sandbox: Sandbox) -> None:
super().__init__()
self._sandbox = sandbox

def _get_sandbox(self) -> Sandbox:
return self._sandbox


class ExistingSandboxClaimLifecycleManager(K8sAgentSandboxLifecycleManager):
"""Simple manager that uses existing sandbox by its claim, without managing it."""
def __init__(
self,
client: SandboxClient,
claim_name: str,
namespace: str,
) -> None:
super().__init__()
self._client = client
self._claim_name = claim_name
self._namespace = namespace

def _get_sandbox(self) -> Sandbox:
return self._client.get_sandbox(
self._claim_name,
namespace=self._namespace
)


class LabelScopedLifecycleManager(K8sAgentSandboxLifecycleManager):
"""
Fully manage the lifecycle of a sandbox based on the provided scope.
Concurrent first runs for the same scope may create duplicate sandboxes, subsequent runs then fail until the duplicates are deleted.
"""
def __init__(
self,
client: SandboxClient,
sandbox_settings: K8sAgentSandboxSettings,
scope: dict[str, str],
scope_labels_prefix: str,
) -> None:
super().__init__()
self._client = client
self._sandbox_settings = sandbox_settings

self._scope_labels = {
f"{scope_labels_prefix}/{k}": v for k, v in scope.items()
}

self._scope_label_selector = ",".join(
[f"{k}={v}" for k, v in self._scope_labels.items()]
)

def _get_sandbox(self) -> Sandbox:
claim_name = self._find_sandbox_claim_by_label_selector()

if claim_name is not None:
try:
return self._client.get_sandbox(
claim_name,
namespace=self._sandbox_settings.namespace
)
except SandboxNotFoundError:
if self._client.k8s_helper.get_sandbox_claim(
claim_name, namespace=self._sandbox_settings.namespace
) is not None:
raise

labels = dict(self._sandbox_settings.labels or {})
labels.update(self._scope_labels)

return self._client.create_sandbox(
self._sandbox_settings.warmpool,
namespace=self._sandbox_settings.namespace,
sandbox_ready_timeout=self._sandbox_settings.sandbox_ready_timeout,
labels=labels,
shutdown_after_seconds=self._sandbox_settings.shutdown_after_seconds,
volume_claim_templates=self._sandbox_settings.volume_claim_templates,
pod_labels=self._sandbox_settings.pod_labels,
pod_annotations=self._sandbox_settings.pod_annotations,
)

def _find_sandbox_claim_by_label_selector(self) -> str | None:

found = self._client.list_all_sandboxes(
namespace=self._sandbox_settings.namespace,
label_selector=self._scope_label_selector,
)

if len(found) > 1:
raise RuntimeError(
f"Expected 1 sandbox for scope {self._scope_labels}, found {len(found)}: "
f"{found}. Delete orphan sandboxes manually."
)

if len(found) == 1:
return found[0]

if len(found) == 0:
return None
Loading