OS-enforced sandbox backend for LangChain Deep Agents using nono.
Kernel-level sandboxing, network filtering, policy-based access control, credential injection, and filesystem snapshots — all native Python, no containers required.
Installation
pip install langchain-nono
Usage
import uuid
from pathlib import Path
from langchain_anthropic import ChatAnthropic
from deepagents import create_deep_agent
from langchain_nono import NonoSandbox
thread_id = str(uuid.uuid4())
working_dir = Path("/tmp/agent-sandboxes") / thread_id
working_dir.mkdir(parents=True, exist_ok=True)
sandbox = NonoSandbox(
working_dir=str(working_dir),
virtual_workspace_root=True,
)
agent = create_deep_agent(
backend=sandbox,
model=ChatAnthropic(model_name="claude-sonnet-4-6"),
system_prompt="You are a coding assistant with sandbox access.",
)
result = agent.invoke(
{
"messages": [
{
"role": "user",
"content": "Create a hello world Python script and run it",
}
]
},
config={"configurable": {"thread_id": thread_id}},
)
print(result["messages"][-1].content)
virtual_workspace_root=True lets Deep Agents use absolute tool paths such as
/hello.py while langchain-nono stores the file under the concrete per-thread
workspace, for example /tmp/agent-sandboxes/<thread-id>/hello.py.
Configuration
sandbox = NonoSandbox(
working_dir="/tmp/agent-workspace", # Required: read-write access
virtual_workspace_root=True, # Map /file.py to working_dir/file.py for Deep Agents
allow_read=["/data/models"], # Additional read-only paths
allow_readwrite=["/tmp/scratch"], # Additional read-write paths
policy_json=json.dumps({ # Optional: nono policy JSON
"groups": {
"project_rw": {
"description": "RW access to a project directory",
"allow": {"readwrite": ["/tmp/agent-workspace"]}
}
}
}),
policy_groups=["project_rw"], # Groups to resolve from policy_json
proxy_config=ProxyConfig( # Optional: host filtering + credential injection
allowed_hosts=["api.openai.com"],
),
snapshot_session_dir="/tmp/nono-session", # Optional: enable snapshots + rollback
block_network=True, # Block outbound network (default)
timeout=300, # Default command timeout in seconds
)
Network Filtering
Pass proxy_config=ProxyConfig(...) to start the nono proxy when the sandbox is
created. execute() automatically receives the proxy environment variables, so
host filtering and credential injection apply to sandboxed child processes
without extra wiring in the caller.
import shlex
from langchain_nono import NonoSandbox, ProxyConfig
sandbox = NonoSandbox(
working_dir="/tmp/agent-workspace",
proxy_config=ProxyConfig(allowed_hosts=["example.com"]),
block_network=True,
)
request_script = """
import urllib.request
with urllib.request.urlopen("https://example.com", timeout=30) as response:
print(response.status)
"""
result = sandbox.execute(f"python3 -c {shlex.quote(request_script)}")
print(result.exit_code)
events = sandbox.drain_network_audit_events()
sandbox.shutdown_proxy()
Or resolve proxy config from a policy file:
proxy_config = NonoSandbox.resolve_proxy_from_policy(
policy_json, ["proxy_web_demo"]
)
Credential Injection
The proxy can inject real API credentials on outbound requests, so
sandboxed code never sees real keys. Real credentials can be loaded from
host-side sources such as env://OPENAI_API_KEY. When env_var is configured
on a route, the sandboxed child receives a route-scoped phantom token in that
variable; the proxy swaps that phantom token for the real credential before
forwarding upstream.
When proxy mode is enabled, langchain-nono uses nono-py's proxy-only
network mode so sandboxed code can connect only to the local proxy port;
all direct outbound network access remains blocked.
import shlex
from langchain_nono import InjectMode, NonoSandbox, ProxyConfig, RouteConfig
sandbox = NonoSandbox(
working_dir="/tmp/agent-workspace",
proxy_config=ProxyConfig(
allowed_hosts=["api.openai.com"],
routes=[
RouteConfig(
prefix="/openai",
upstream="https://api.openai.com",
credential_key="env://OPENAI_API_KEY", # Host env lookup
inject_mode=InjectMode.HEADER,
inject_header="Authorization",
credential_format="Bearer {}",
env_var="OPENAI_API_KEY", # Phantom token env var
)
],
),
block_network=True,
)
try:
# The child sees OPENAI_API_KEY=<phantom> and OPENAI_BASE_URL=http://127.0.0.1:<port>/openai.
# The proxy swaps the phantom token for the real key on outbound requests.
request_script = """
import os
import urllib.request
request = urllib.request.Request(
os.environ["OPENAI_BASE_URL"] + "/v1/models",
headers={"Authorization": "Bearer " + os.environ["OPENAI_API_KEY"]},
)
with urllib.request.urlopen(request, timeout=30) as response:
print(response.read().decode())
"""
result = sandbox.execute(f"python3 -c {shlex.quote(request_script)}")
print(result.output)
finally:
sandbox.shutdown_proxy()
Injection modes: HEADER, QUERY_PARAM, BASIC_AUTH, URL_PATH.
Snapshots
Pass snapshot_session_dir=... to enable content-addressable snapshots and
rollback for the sandbox workspace.
from pathlib import Path
from tempfile import TemporaryDirectory
from langchain_nono import ExclusionConfig, NonoSandbox
def print_changes(title, changes):
print(title)
for change in changes:
print(f" - {change.change_type}: {Path(change.path).name}")
with (
TemporaryDirectory(prefix="agent-workspace-") as workspace,
TemporaryDirectory(prefix="nono-session-") as session_dir,
):
sandbox = NonoSandbox(
working_dir=workspace,
snapshot_session_dir=session_dir,
snapshot_exclusion=ExclusionConfig(exclude_patterns=["node_modules"]),
)
sandbox.execute("printf 'version 1\n' > app.txt")
baseline = sandbox.create_snapshot_baseline()
print("Baseline snapshot")
print(f" app.txt contains: {sandbox.execute('cat app.txt').output.strip()!r}")
sandbox.execute("printf 'version 2\n' > app.txt")
sandbox.execute("printf 'generated\n' > output.txt")
manifest, changes = sandbox.create_snapshot_incremental()
print("\nAgent changed the workspace")
print(f" app.txt now contains: {sandbox.execute('cat app.txt').output.strip()!r}")
print_changes("Snapshot detected:", changes)
print(f" Merkle root changed: {baseline.merkle_root != manifest.merkle_root}")
diff = sandbox.compute_restore_diff(0)
print_changes("\nDry-run restore preview:", diff)
restored = sandbox.restore_snapshot(0)
print(f"\nRestored baseline by applying {len(restored)} change(s)")
print(f" app.txt contains: {sandbox.execute('cat app.txt').output.strip()!r}")
print(
" output.txt:",
sandbox.execute("test -e output.txt && echo exists || echo removed").output.strip(),
)
Session Metadata
Save audit trails with Merkle roots and network events:
meta = SessionMetadata(
session_id="my-session",
command=["bash", "-c", "echo hello"],
tracked_paths=["/tmp/agent-workspace"],
)
meta.add_merkle_root(baseline.merkle_root)
sandbox.save_session_metadata(meta)
# Later, load from disk:
loaded = NonoSandbox.load_session_metadata("/tmp/nono-session")
Examples
Inline policy for an agent that can write in its workspace, read a reference folder, and is denied access to a sibling secrets folder because that path is never granted:
python examples/01_policy_inline.py
Policy loaded from a JSON file with the same workspace/reference split, plus an
explicit deny.access rule for the secrets folder on macOS:
python examples/02_policy_from_file.py
Policy-aware upload_files() and download_files() with user-facing error
messages instead of raw backend error codes:
python examples/03_policy_file_transfer.py
Proxy basics -- starting a proxy, running commands, draining audit events:
python examples/04_proxy_basics.py
API key protection via proxy credential injection without exposing the API key:
python examples/05_credential_injection.py
Policy-based proxy configuration resolved from JSON groups:
python examples/06_policy_proxy.py
Filesystem snapshots with dry-run diff and rollback:
python examples/07_snapshot_rollback.py
Full supervisor flow combining proxy, snapshots, and session metadata:
python examples/08_proxy_with_snapshots.py
The matching policy document is:
examples/policy_example.json
How it works
Each execute() call:
- Forks the current process
- Applies OS-level sandbox restrictions in the child (Landlock or Seatbelt)
- Exec's the command
- Captures stdout/stderr and waits for exit
The parent process remains unsandboxed and can call execute() repeatedly. Sandbox restrictions are enforced by the kernel and cannot be bypassed from userspace.
Platform support
| Platform | Mechanism | Minimum version |
|---|---|---|
| Linux | Landlock LSM | Kernel 5.13+ |
| macOS | Seatbelt | macOS 10.15+ |
Metadata
Release files for langchain-nono 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| langchain_nono-0.4.0.tar.gz | 200.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| langchain_nono-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 217.6 kB
Release files / langchain_nono-0.4.0.tar.gz
| Download URL | langchain_nono-0.4.0.tar.gz |
|---|---|
| Size | 200.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
15a0b94937df7f326ee30e79e27a5bdc8beabf1054bfc89dca85d4a9510de31a
|
|
BLAKE2b-256 checksum How to use checksums |
bb15283a4791e9b807d302149dddf6debcf64c9d9c5b42536397e9efb95698b9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 11, 2026.
Transparency logRelease files / langchain_nono-0.4.0-py3-none-any.whl
| Download URL | langchain_nono-0.4.0-py3-none-any.whl |
|---|---|
| Size | 16.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1a541a01ed87910e11c776d65e79fae5741472c049f0379f1366af6fc3373c40
|
|
BLAKE2b-256 checksum How to use checksums |
3ce833894dd97037c6b6ef63cdba3d6eb067a76eeae4774c14c237b2fda931ef
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 11, 2026.
Transparency log