> ## Documentation Index
> Fetch the complete documentation index at: https://nono.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Module Functions

> Top-level functions in nono_py

The `nono_py` module provides functions for applying sandboxes, starting the network proxy, and checking platform support.

It also provides policy-loading helpers for filesystem and network policy resolution.

## apply

```python theme={null}
apply(caps: CapabilitySet) -> None
```

Apply the sandbox with the given capabilities. **This is irreversible.**

Once called, the current process and all child processes can only access resources granted by the capabilities. There is no way to expand permissions after this call.

<ParamField path="caps" type="CapabilitySet" required>
  The capability set defining permitted operations.
</ParamField>

**Raises:**

* `RuntimeError` - Platform not supported or sandbox initialization failed

### Example

```python theme={null}
from nono_py import CapabilitySet, AccessMode, apply

caps = CapabilitySet()
caps.allow_path("/tmp", AccessMode.READ_WRITE)
caps.block_network()

# After this call, the process is sandboxed
apply(caps)

# These work:
with open("/tmp/test.txt", "w") as f:
    f.write("Hello")

# These fail with PermissionError:
open("/etc/passwd", "r")
```

<Warning>
  **No escape hatch.** Once `apply()` succeeds:

  * Permissions cannot be expanded
  * The sandbox persists until process exit
  * All child processes inherit the same restrictions
  * There is no "undo" or "disable" function
</Warning>

### Error Handling

```python theme={null}
from nono_py import CapabilitySet, AccessMode, apply, is_supported

caps = CapabilitySet()
caps.allow_path("/tmp", AccessMode.READ_WRITE)

if not is_supported():
    print("Warning: sandboxing not available, running without protection")
else:
    try:
        apply(caps)
        print("Sandbox applied successfully")
    except RuntimeError as e:
        print(f"Failed to apply sandbox: {e}")
        exit(1)
```

***

## is\_supported

```python theme={null}
is_supported() -> bool
```

Check if sandboxing is supported on this platform.

**Returns:** `True` if sandboxing is available.

### Example

```python theme={null}
from nono_py import is_supported

if is_supported():
    print("Sandbox available")
else:
    print("Sandbox not available")
```

### Platform Requirements

| Platform | Requirement                            |
| -------- | -------------------------------------- |
| Linux    | Kernel 5.13+ with Landlock enabled     |
| macOS    | macOS 10.5+ (always available)         |
| Windows  | Not supported (always returns `False`) |

<Tip>
  Use `support_info()` for more detailed information about why sandboxing might not be available.
</Tip>

***

## support\_info

```python theme={null}
support_info() -> SupportInfo
```

Get detailed information about sandbox support on this platform.

**Returns:** `SupportInfo` object with platform details.

### Example

```python theme={null}
from nono_py import support_info

info = support_info()

print(f"Platform: {info.platform}")
print(f"Supported: {info.is_supported}")
print(f"Details: {info.details}")
```

Output examples:

**macOS:**

```
Platform: macos
Supported: True
Details: Seatbelt sandbox available
```

**Linux with Landlock:**

```
Platform: linux
Supported: True
Details: Landlock ABI v5 available
```

**Linux without Landlock:**

```
Platform: linux
Supported: False
Details: Landlock not available (kernel too old)
```

### Detailed Platform Check

```python theme={null}
from nono_py import support_info
import sys

def require_sandbox():
    """Exit if sandboxing is not available."""
    info = support_info()

    if info.is_supported:
        return

    print(f"Error: Sandboxing required but not available", file=sys.stderr)
    print(f"Platform: {info.platform}", file=sys.stderr)
    print(f"Details: {info.details}", file=sys.stderr)

    if info.platform == "linux":
        print("\nTo enable Landlock on Linux:", file=sys.stderr)
        print("  1. Upgrade to kernel 5.13 or later", file=sys.stderr)
        print("  2. Ensure CONFIG_SECURITY_LANDLOCK=y in kernel config", file=sys.stderr)

    sys.exit(1)

require_sandbox()
```

***

## sandboxed\_exec

```python theme={null}
sandboxed_exec(
    caps: CapabilitySet,
    command: list[str],
    cwd: str | None = None,
    timeout_secs: float | None = None,
    env: list[tuple[str, str]] | None = None,
    inherit_env: bool = False,
    max_processes: int | None = None,
) -> ExecResult
```

Run a command in a sandboxed child process while the Python caller remains
unsandboxed. The child receives only the capabilities in `caps`.

<ParamField path="caps" type="CapabilitySet" required>
  Capability set defining the child's permitted filesystem and network access.
</ParamField>

<ParamField path="command" type="list[str]" required>
  Program and arguments to execute, for example `["python", "agent.py"]`.
</ParamField>

<ParamField path="cwd" type="str | None">
  Working directory for the child process.
</ParamField>

<ParamField path="timeout_secs" type="float | None">
  Maximum runtime in seconds. On timeout, `sandboxed_exec()` kills the
  sandboxed command's process group and returns exit code `124`.
</ParamField>

<ParamField path="env" type="list[tuple[str, str]] | None">
  Explicit child environment variables. The parent environment is not inherited
  unless `inherit_env=True`.
</ParamField>

<ParamField path="inherit_env" type="bool">
  If true, inherit the parent environment and apply `env` as overrides.
  Dynamic-loader environment variables such as `LD_*` and `DYLD_*` are rejected.
</ParamField>

<ParamField path="max_processes" type="int | None">
  Optional `RLIMIT_NPROC` value for the child. This is a per-real-UID kernel
  limit, not a per-sandbox process-tree limit.
</ParamField>

**Returns:** `ExecResult` with `stdout`, `stderr`, and `exit_code`.

**Raises:**

* `RuntimeError` - Fork failed, sandbox setup failed, or command could not be executed
* `ValueError` - Command is empty, timeout is negative, or `max_processes` is zero

### Example

```python theme={null}
from nono_py import AccessMode, CapabilitySet, sandboxed_exec

caps = CapabilitySet()
caps.allow_path("/workspace", AccessMode.READ_WRITE)
caps.block_network()

result = sandboxed_exec(
    caps,
    ["python", "agent.py"],
    cwd="/workspace",
    timeout_secs=30.0,
)
print(result.exit_code, result.stdout)
```

### Optional Process Limit

`max_processes` sets `RLIMIT_NPROC` before `exec`. Use it only when sandboxed
executions run under a dedicated Unix UID. The kernel counts every process
already owned by that UID, so on a normal shared user account a small value can
make the sandboxed program's first `fork()` fail with `EAGAIN` instead of
providing a per-sandbox process budget.

```python theme={null}
# Dedicated sandbox UID/container only.
result = sandboxed_exec(
    caps,
    ["python", "agent.py"],
    cwd="/workspace",
    timeout_secs=30.0,
    max_processes=64,
)
```

Use cgroup `pids.max`, containers, or microVMs for reliable per-execution fork
limits.

***

## start\_proxy

```python theme={null}
start_proxy(config: ProxyConfig) -> ProxyHandle
```

Start the nono network filtering proxy.

Creates a localhost proxy server that provides domain filtering, credential injection, and audit logging for sandboxed child processes. The proxy runs on a background thread and is shut down via the returned handle.

<ParamField path="config" type="ProxyConfig" required>
  Proxy configuration including allowed hosts, credential routes, and bind settings.
</ParamField>

**Returns:** `ProxyHandle` with `env_vars()`, `credential_env_vars()`, `drain_audit_events()`, and `shutdown()`.

**Raises:**

* `RuntimeError` - Proxy failed to start (port in use, etc.)

### Example

```python theme={null}
from nono_py import ProxyConfig, RouteConfig, start_proxy, sandboxed_exec

config = ProxyConfig(
    allowed_hosts=["api.openai.com"],
    routes=[
        RouteConfig(
            prefix="/openai",
            upstream="https://api.openai.com",
            credential_key="openai-key",
        ),
    ],
)
proxy = start_proxy(config)

# Inject env vars into sandboxed child
env = list(proxy.env_vars().items()) + list(proxy.credential_env_vars().items())
result = sandboxed_exec(caps, ["python", "agent.py"], env=env)
# Optional for dedicated sandbox UIDs only:
# result = sandboxed_exec(caps, ["python", "agent.py"], env=env, max_processes=64)

# Collect audit trail and shut down
events = proxy.drain_audit_events()
proxy.shutdown()
```

***

## embedded\_policy\_json

```python theme={null}
embedded_policy_json() -> str
```

Return the bundled policy JSON document as a raw string.

**Returns:** JSON text for the embedded policy.

### Example

```python theme={null}
from nono_py import embedded_policy_json

raw = embedded_policy_json()
print(raw[:80])
```

***

## load\_policy

```python theme={null}
load_policy(json: str) -> Policy
```

Parse a policy JSON document into a `Policy` object.

**Returns:** `Policy` with group inspection and resolution helpers.

**Raises:**

* `ValueError` - Invalid policy JSON, including unknown policy fields

### Example

```python theme={null}
from nono_py import load_policy

policy = load_policy(
    """
    {
      "groups": {
        "offline": {
          "description": "Disable outbound network",
          "network": { "block": true }
        },
        "proxy_web": {
          "description": "Allow only example.com through the proxy",
          "network": { "allow_proxy": ["example.com"] }
        }
      }
    }
    """
)

print(policy.group_names())
proxy_config = policy.resolve_proxy_config(["proxy_web"])
print(proxy_config.allowed_hosts if proxy_config else [])
```

`Policy.resolve_proxy_config()` follows the same network field naming used by
the main nono profile format:

* Canonical key: `network.allow_domain`
* Accepted aliases: `network.allow_proxy`, `network.proxy_allow`

If multiple groups specify different `upstream_proxy` / `external_proxy`
values, resolution fails with `ValueError` instead of silently overwriting the
earlier proxy.

Policy JSON is parsed strictly. Unknown fields are rejected instead of being
ignored.

When resolving groups with proxy-filtered network policy, `resolve_groups()`
fails closed by blocking direct network access. After starting the proxy, call
`caps.proxy_only(proxy)` and pass `proxy.sandbox_env()` to `sandboxed_exec()`.

***

## load\_embedded\_policy

```python theme={null}
load_embedded_policy() -> Policy
```

Parse the bundled policy JSON and return it as a `Policy`.

### Example

```python theme={null}
from nono_py import load_embedded_policy

policy = load_embedded_policy()
print("deny_credentials" in policy.group_names())
```

***

## Import

All functions are available from the top-level module:

```python theme={null}
from nono_py import (
    apply,
    embedded_policy_json,
    is_supported,
    load_embedded_policy,
    load_policy,
    sandboxed_exec,
    start_proxy,
    support_info,
)
```

## Related

* [CapabilitySet](/docs/python/api/capability-set) - Build capabilities to pass to `apply()`
* [ProxyConfig](/docs/python/api/proxy-config) - Configure the network proxy
* [SnapshotManager](/docs/python/api/snapshot-manager) - Filesystem snapshots and rollback
* [SupportInfo](/docs/python/api/support-info) - Returned by `support_info()`
* [QueryContext](/docs/python/api/query-context) - Check permissions without applying
