Metadata-Version: 2.4
Name: runbound-runtime
Version: 0.1.0a1
Summary: Runbound Runtime for CPython: runs a process under policy, deciding file, network, process, environment and dynamic-code operations and recording them as audit evidence.
Author: Runbound
License-Expression: LicenseRef-Runbound-Runtime-SDK AND ISC AND Unicode-3.0
Project-URL: Homepage, https://github.com/runbound-io/runtime/tree/main/adapters/python#readme
Project-URL: Repository, https://github.com/runbound-io/runtime
Project-URL: Issues, https://github.com/runbound-io/runtime/issues
Project-URL: Changelog, https://github.com/runbound-io/runtime/blob/main/adapters/python/CHANGELOG.md
Classifier: Development Status :: 3 - Alpha
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: <3.15,>=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: THIRD_PARTY_NOTICES.md
Requires-Dist: runbound-application-context==0.1.0a1
Provides-Extra: cloud
Requires-Dist: cryptography<51,>=50.0.0; extra == "cloud"
Dynamic: license-file

# runbound-runtime

Apply a Runbound policy to a CPython process. File, network, process,
environment and dynamic-code operations are checked against the policy; a
refused operation raises `PermissionError` and the decision is recorded.

## About Runbound

Runbound runs workloads under policy and records what happened as evidence.
This package is its guard for CPython processes.

## Install

```sh
python -m pip install --pre runbound-runtime
```

`--pre` selects the alpha. pip also installs `runbound-application-context`,
which carries the operation attribution described under the API table.

## Alpha limits

`0.1.0a1` is an early, unsupported alpha. APIs, policy fields and evidence
formats may change between alpha releases.

- **Ordinary CPython 3.12, 3.13 or 3.14.** pip refuses other versions, and
  free-threaded or debug builds and other interpreters fail at startup with
  `BootstrapError`. On a patch below Runbound's reviewed security floor for its
  line, the Runtime still enforces and records `runtime.adapter.degraded`.
- **Restricting `python.ffi` needs CPython 3.14.** A policy that sets
  `python.ffi` to `deny` or `audit` refuses to start on 3.12 and 3.13 with
  `runtime.python_ffi_unavailable`, because those versions cannot audit every
  ctypes call. `allow` works on every supported version.
- **Pure Python, tested on Linux x86-64.** It installs on any platform, and
  macOS is not yet tested. On Windows it refuses a policy file, because the
  rules that resolve the paths in it are proven for POSIX paths only.
- **Cloud mode is not yet available.** This build carries no Runbound
  server-identity roots, so `CloudOptions` refuses Runbound's production
  origins before connecting and no credential leaves the host. Keep evidence
  local with an audit log.

## Quickstart

This runs entirely on your machine and needs no network. Save the policy as
`runbound.yaml`; it allows connections to `127.0.0.1` and refuses every other
destination:

```yaml
schemaVersion: 1
policy:
  mode: enforce
  access:
    network:
      http:
        allow:
          - 127.0.0.1
      unmatched: refuse
```

Save this as `app.py`. It installs the Runtime before anything else, serves one
page on a free local port, then requests it and another host:

```python
from pathlib import Path

from runbound_runtime import StandaloneOptions, install_standalone

here = Path.cwd()
install_standalone(
    StandaloneOptions(
        policy_path=str(here / "runbound.yaml"),
        audit_log=str(here / "runbound-events.jsonl"),
    )
)

import http.client
import http.server
import threading


class Hello(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b"hello")

    def log_message(self, *args):
        pass


server = http.server.HTTPServer(("127.0.0.1", 0), Hello)
threading.Thread(target=server.serve_forever, daemon=True).start()


def get(host, port):
    connection = http.client.HTTPConnection(host, port, timeout=5)
    try:
        connection.request("GET", "/")
        response = connection.getresponse()
        print(f"{host}: {response.status} {response.read().decode()}")
    except PermissionError as error:
        print(f"{host}: refused ({error.capability})")
    finally:
        connection.close()


get("127.0.0.1", server.server_port)
get("example.com", 80)
server.shutdown()
```

```sh
python app.py
```

Expected output:

```text
127.0.0.1: 200 hello
example.com: refused (network.request)
```

The refusal happens before Python looks up `example.com` or opens a socket.
Every decision, allowed or refused, is appended to `runbound-events.jsonl`.
The rule names no port, so it matches whichever port the server takes;
`127.0.0.1:8080` would pin one. A process that installs nothing is not
guarded: the package does nothing until `install_standalone` runs.

The policy file is the same document every Runbound Runtime reads, as
`runbound.yaml`, `runbound.yml` or `runbound.json`, decoded by its extension; a
JSON policy never loads the YAML parser. What it leaves out takes its default:
no file access, Python's own capabilities allowed, and starting or inspecting
processes denied. Relative paths in it resolve from the file's directory. The
Runtime keeps the normalized result under `.runbound/cache` beside the file, so
an unchanged policy is not decoded again, and rebuilds it when the file changes.

## API

| API | Purpose |
| --- | --- |
| `install_standalone(StandaloneOptions(policy_path, audit_log=...))` | Install the guard from a policy file, recording to a local audit log. |
| `flush()`, `uninstall()`, `status()` | Drain the audit log, remove the guard, and report whether it is installed. |
| `Policy.from_json(data)` | Parse the normalized policy a Runbound supervisor passes in `RUNBOUND_POLICY_JSON`; `policy.network(...)`, `.filesystem(...)` and `.environment(...)` decide without installing anything. |
| `WSGIMiddleware`, `ASGIMiddleware`, `trace_workload` | Trace requests and workloads. |
| `application_scope(application_user_id=..., release_id=...)` | Attribute an operation to your application's user and release. |
| `runbound_lambda_entry.handler`, `runbound_faas_entry.handler` | Entry points for AWS Lambda and Lambda@Edge; `runbound_cloud_run_entry` and `runbound_azure_functions_entry` serve Google Cloud Run functions and Azure Functions. |

A refused operation raises a `PermissionError` subclass whose `capability`,
`operation`, `target` and `reason` name the decision. A malformed policy raises
`PolicyError`; treat it as a startup failure.

## Network access

This package makes no outbound connections of its own while Cloud mode is
off; the policy decides your application's. Rules live under
`policy.access.network`:

| Rule | Matches |
| --- | --- |
| `http.allow`, `http.deny` | A host (`api.example.com`), a wildcard (`*.example.com`), a host and port (`api.example.com:443`) or a URL's scheme, host and port (`https://api.example.com`). |
| `tcp.allow`, `tcp.deny` | Raw TCP: addresses, CIDR blocks (`10.0.0.0/8`), address ranges, name wildcards, port lists and `tcp://` URLs. |
| `unmatched` | `refuse` refuses what no rule names; the default, `policy-miss`, refuses it as a policy miss. |

A rule without a port matches every port. A deny that names its target wins
over any allow; then an allow admits; then a universal `*` deny refuses. Paths,
methods and query strings cannot be decided at a socket, so a rule that names
one fails to load, as does a host or port spelled any way other than its
canonical form. A host with no single spelling, such as `127.1` or
`0x7f.1`, is refused before any rule is read.

The guard covers Internet stream sockets: `socket.connect`, `connect_ex`,
`socket.create_connection`, and clients built on them, such as `http.client`,
`urllib`, `requests`, `urllib3` and `asyncio`. A name is decided before it is
looked up, and every address a lookup returns is checked against the deny rules
before any is dialed. Datagram, raw and non-Internet sockets cannot use an HTTP
or TCP allow.

`urllib` wraps a refusal in `URLError`, whose `reason` is the
`PermissionError`. It also reads `REQUEST_METHOD`, proxy variables,
`SSL_CERT_FILE`, `SSL_CERT_DIR` and `SSLKEYLOGFILE`, which the policy refuses
unless `policy.access.env.allow` names them, as in
`[REQUEST_METHOD, "*_proxy", "*_PROXY", "SSL_*", SSLKEYLOGFILE]`. `PATH`,
`HOME`, user, shell, terminal and temporary-directory variables, `PYTHON*` and
`RUNBOUND_*` are always readable.

Start with `mode: audit`, which records every decision and refuses
nothing, then allow only the destinations the records show you need.

## What it does not do

The guard is cooperative and in-process. Native extensions, direct system
calls, sockets or files opened before `install_standalone`, inherited
descriptors and replacing the interpreter bypass it. It does not sandbox a
process; run untrusted code under an operating-system boundary as well.

## Upgrade and uninstall

Pin the version: `runbound-runtime==0.1.0a1`. Upgrade one alpha at a time and
check your policy loads before rolling forward; roll back by pinning the
earlier version. To uninstall, call `uninstall()`, then
`python -m pip uninstall runbound-runtime runbound-application-context`.

Installing the package adds no startup hook. Its `sitecustomize` module lives
in `runbound_runtime/preload`, which Python reads only when `PYTHONPATH` names
it, as Runbound Core and the managed-function guides do; any other
`sitecustomize` in the environment is left alone.

Source: [runbound-io/runtime](https://github.com/runbound-io/runtime/tree/main/adapters/python).
Runbound software is proprietary under the Runbound Runtime and SDK Licence in this package's `LICENSE` file. The licence permits use in your own commercial and internal applications and distribution as an application dependency; it does not permit standalone redistribution. Cloud-required features need the appropriate Runbound service entitlement. Source is intended to be public for inspection and contributions through GitHub issues and pull requests; follow the repository contribution terms. Third-party work the package carries, a port of the `yaml`
parser and Unicode data, is listed in `THIRD_PARTY_NOTICES.md`; changes are in
[CHANGELOG.md](https://github.com/runbound-io/runtime/blob/main/adapters/python/CHANGELOG.md).
