OS-enforced sandbox backend for LangChain Deep Agents using Landlock (Linux) and Seatbelt (macOS)
Project description
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 json
from deepagents import create_deep_agent
from langchain_nono import NonoSandbox
from nono_py import 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="openai-key",
)
],
),
block_network=True,
)
agent = create_deep_agent(
backend=sandbox,
system_prompt="You are a coding assistant.",
)
Configuration
sandbox = NonoSandbox(
working_dir="/tmp/agent-workspace", # Required: read-write access
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.
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="openai-key",
inject_mode=InjectMode.HEADER,
)
],
),
block_network=True,
)
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 transparently swap phantom tokens for real API credentials, so sandboxed code never sees real keys. Real credentials are loaded from the host's OS keyring; only phantom tokens enter the sandbox.
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="openai-key", # OS keyring lookup
inject_mode=InjectMode.HEADER,
inject_header="Authorization",
credential_format="Bearer {}",
)
],
),
block_network=True,
)
# 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.
result = sandbox.execute("curl $OPENAI_BASE_URL/v1/models -H 'Authorization: Bearer $OPENAI_API_KEY'")
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 langchain_nono import ExclusionConfig, NonoSandbox, SessionMetadata
sandbox = NonoSandbox(
working_dir="/tmp/agent-workspace",
snapshot_session_dir="/tmp/nono-session",
snapshot_exclusion=ExclusionConfig(exclude_patterns=["node_modules"]),
)
baseline = sandbox.create_snapshot_baseline()
manifest, changes = sandbox.create_snapshot_incremental()
diff = sandbox.compute_restore_diff(0) # dry-run preview
restored = sandbox.restore_snapshot(0) # actual rollback
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 with phantom token swapping:
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+ |
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file langchain_nono-0.2.1.tar.gz.
File metadata
- Download URL: langchain_nono-0.2.1.tar.gz
- Upload date:
- Size: 194.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
946cfb6b2703b8f0791fe8ca7ed7835c3a50ffd23f5530222c2e9d7df37e2fdb
|
|
| MD5 |
d026ef9d935abfa0fcbdf73c55692669
|
|
| BLAKE2b-256 |
60010c968ecfb2efb88575973dbe4c2b8fc1c9076628758efee20759247347f3
|
Provenance
The following attestation bundles were made for langchain_nono-0.2.1.tar.gz:
Publisher:
publish.yml on always-further/langchain-nono
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
langchain_nono-0.2.1.tar.gz -
Subject digest:
946cfb6b2703b8f0791fe8ca7ed7835c3a50ffd23f5530222c2e9d7df37e2fdb - Sigstore transparency entry: 1342715625
- Sigstore integration time:
-
Permalink:
always-further/langchain-nono@00882c0bdd8dc9888e2613abf9286c915ac3ce62 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/always-further
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@00882c0bdd8dc9888e2613abf9286c915ac3ce62 -
Trigger Event:
release
-
Statement type:
File details
Details for the file langchain_nono-0.2.1-py3-none-any.whl.
File metadata
- Download URL: langchain_nono-0.2.1-py3-none-any.whl
- Upload date:
- Size: 13.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0b7ce705d94b7a851931a5cf3337a537f1770cc1c9fba28ed1df6f58b72feda8
|
|
| MD5 |
ef68b7647adc8b74bab30883b10634bf
|
|
| BLAKE2b-256 |
6a32f9a85ec12fae417ad3d4615419e4aa6f10f61e2af1270e945af748a64a30
|
Provenance
The following attestation bundles were made for langchain_nono-0.2.1-py3-none-any.whl:
Publisher:
publish.yml on always-further/langchain-nono
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
langchain_nono-0.2.1-py3-none-any.whl -
Subject digest:
0b7ce705d94b7a851931a5cf3337a537f1770cc1c9fba28ed1df6f58b72feda8 - Sigstore transparency entry: 1342715631
- Sigstore integration time:
-
Permalink:
always-further/langchain-nono@00882c0bdd8dc9888e2613abf9286c915ac3ce62 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/always-further
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@00882c0bdd8dc9888e2613abf9286c915ac3ce62 -
Trigger Event:
release
-
Statement type: