> For the complete documentation index, see [llms.txt](https://govplane.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://govplane.gitbook.io/docs/documentation/resources/the-python-runtime-sdk-joins-govplane-v2.md).

# The Python Runtime SDK joins Govplane v2

Version 2.0 of the Govplane Python Runtime SDK is out. It brings the Python runtime onto the same architecture as the rest of v2, and the headline is the same one as the rest of v2: production evaluation is yours. Loading a bundle from disk is now the ordinary path, Govplane Cloud is an option you can take or leave, and nothing in the evaluation path needs an account, a credential or a network call.

This is a rewrite, not an increment on 0.1.0. The package name is the same and the version jumps to 2.0.0 to line up with the Node SDK, but very little carries over unchanged.

### The old shape, and why it had to go

The 0.1.0 SDK had one constructor, and it required cloud credentials whatever you were doing. You passed a `base_url` and a `runtime_key` even to evaluate a JSON file sitting next to your application. If you wanted the offline path, you opted into a separate "isolated mode" with its own config class, its own environment variable and a trigger file at `/etc/govplane/isolated.mode`. Local evaluation was the special case, and it cost you a second code path to get to.

That is backwards for a runtime whose whole promise is that it works without us. In 2.0 the two ways in are two factories, and the local one is the smaller of the two.

```python
from govplane_sdk import EcdsaKey, Target, create_local_client

with create_local_client(
    "policy-bundle.json",
    verify=EcdsaKey(public_key=open("public-key.pem").read()),
) as govplane:
    decision = govplane.evaluate(
        Target("api", "orders", "read"),
        context={"plan": "pro", "failedAttempts": 2},
    )
    print(decision.decision)  # "allow"
```

That is the whole thing. Build a bundle with `govplane build`, sign it with `govplane sign` using your own key, ship it however you already ship configuration, and evaluate it here. No account, no credentials, no runtime endpoint, no startup handshake.

The remote client is still there for teams that want the control plane, and it is the only place that asks for `org_id`, `project_id` and `env`, because those are how the control plane addresses a bundle. A local client asks for none of them.

```python
govplane = create_remote_client(
    base_url="https://runtime.govplane.io/bundle",
    runtime_key=os.environ["GOVPLANE_RUNTIME_KEY"],
    org_id="org_...",
    project_id="proj_...",
    env="prod",
    verify=EcdsaKey(public_key=public_key_pem),
)
```

Same engine, same decisions, same error codes. The source of the bundle is a delivery question, not a semantics question.

### Isolated mode is gone, and that is the point

Isolated mode drew a line between bundles signed by the control plane and bundles signed by anything else, and treated the second kind as a degraded fallback for incidents. That was the wrong axis. What matters is whether a bundle verifies against a key you configured. Where the file came from does not change the answer.

So there is no isolated mode in 2.0, no trigger file, no `GOVPLANE_MODE`, and no "safe bundle" vocabulary. A local bundle is a local bundle. The incident kill switch driven by an environment variable is gone too: an SDK that denies everything because a variable is set is a deployment mechanism wearing a policy costume, and it belongs in the layer that sets the variable.

### A client that exists is a client that is ready

There is no `start()` and no `warm_start()`. The bundle is read and verified during construction, so a client you are holding always holds a bundle it has already checked. The unready state that `warm_start(timeout_secs=10)` existed to wait for can no longer occur, which removes a whole category of startup race from your application.

### Verification, and one defect worth naming

Both factories refuse to run without a verification key. If you want to evaluate a bundle whose provenance is not checked, you say so in code with `allow_unsigned=True`, where a reviewer can see it and ask why.

Verification runs structure, then checksum, then signature, and reports one of four outcomes rather than a boolean: a configured key checked the signature and it matched, no signature was present and unsigned bundles were allowed, a key was configured but the bundle carries no signature (raises), or a signature exists and no key was configured (raises unless you opt in). That last case is the subtle one. A signature nobody checked proves nothing, and reporting it as verified would be the most dangerous thing an SDK of this kind could do.

Underneath that sits the defect this release had to fix. Version 0.1.0 shipped two different canonicalisers, and neither matched the signer. One hashed the whole response body, the other hashed the whole document minus `signature`, while `govplane sign` and `gp-worker` both sign a narrower field projection. A bundle produced by the CLI therefore verified nowhere, which is not a small bug in a release built around running without Govplane infrastructure. Version 2.0 has one canonicaliser, asserted byte for byte against the shared signing fixtures, plus a test that builds and signs a bundle with the real `govplane` binary and verifies it here. The absence of that test is what let the implementations drift in the first place.

One consequence to plan for: signatures issued against the 0.1.0 form are now correctly rejected. Re-sign with `govplane sign` or fetch a fresh bundle.

### Parity with the Node SDK, in Python's idiom

Same architecture, same error codes, same decisions, and `decision.to_dict()` gives the camelCase wire shape that matches the Node SDK and `govplane simulate`. Names follow Python convention, so `createLocalClient` becomes `create_local_client` and `evaluateWithTrace` becomes `evaluate_with_trace`.

The factories are synchronous here. Node's are `async` only because `readFile` and `fetch` are, so the Python versions block and work from ordinary synchronous application code with no event loop to arrange. Error codes are shared with `@govplane/cli` and the Node SDK, which means one runbook covers all three. There is a table of every Node to Python name in the migration guide.

### The evaluation surface got wider

* Evaluate a single rule by id: `evaluate(rule_id=..., policy_key=...)`. An unknown rule raises `RuleNotFoundError` instead of returning a denial, because a typo reported as `deny` is a bug nobody ever investigates.
* Shorter answers for call sites that only branch: `as_boolean`, `as_binary`, `effect_only`. A `throttle` is false under `as_boolean`, since it allows the action only within a limit a boolean cannot carry.
* `Decision` is one dataclass rather than a class hierarchy, so you read `decision.decision == "deny"` instead of doing `isinstance` checks.
* Structured traces with `off`, `errors`, `sampled` and `full` levels, a trace budget per window, and a sink that can never fail a decision. A logging mistake must not turn a successful authorisation into an exception.
* `py.typed`, so type checkers downstream can see the annotations.

### Operationally, it behaves

`WatchOptions` reloads a local bundle when the file changes, using modification time and size rather than `inotify`, because `govplane build` writes by atomic rename and native watchers miss that on some platforms. `PollOptions` covers the cloud side, with exponential backoff and jitter that is actually wired up. The 0.1.0 SDK documented backoff settings that nothing ever read.

A failed reload never replaces a good bundle with nothing. A file that stops parsing, or a fetch that fails, is reported through `on_error` while the last verified bundle stays in force. Degraded means "serving the last verified bundle and no longer confident it is current", never "serving nothing", and `status().degraded` tells you when you are in it. Fail closed on trust, fail open on availability.

Watching and polling run on daemon threads, so the SDK can never be the reason a process refuses to exit. `close()` is still worth calling, and the client is a context manager so `with` handles it.

### Corrections that change decisions

Some 0.1.0 behaviour was wrong rather than merely different, and fixing it moves outcomes. Check any rule that leans on these:

* `gte` and `lte` against a missing context key returned true, because the key was coerced to `0`. They now return false. A rule reading `{"op": "lte", "path": "ctx.age", "value": 18}` used to fire for every request that carried no `age` at all.
* `gt`, `gte`, `lt` and `lte` against a non-numeric value are false rather than substituting `0`.
* `in` and `eq` use strict equality, so `true` no longer equals `1`.
* `exists` on a key that is present and `null` is now false.
* A policy default applies only where that policy governs the target. Before, a single deny-by-default policy denied every target in the bundle.
* Context validation is enforced only when you configure a context policy. The built-in default is a seven key sample, and enforcing it rejected the very keys real bundles read.

Keys that look like personal data (`email`, `phone`, `name`, `address` and so on) are still blocked even when allowed, unless you turn that off. The heuristic over-matches on purpose, because a false positive costs one rename and a false negative puts personal data in a trace.

### Removed

* **Ed25519 signatures.** Nothing produced them and AWS KMS cannot, so they could never sit behind the FIPS 140-3 Level 3 claim. Rejected by name, with a message pointing at the replacement.
* **HMAC from Govplane Cloud.** Verifying HMAC requires the secret that produced it, and a cloud signature is produced inside an HSM by a key that never leaves the module. HMAC still works for bundles you sign yourself.
* **Isolated mode**, its trigger file, its environment variable and its vocabulary.
* `RuntimeClient`, `RuntimeClientConfig`, and the requirement for a `base_url` and a `runtime_key` to evaluate a file on disk.

### Upgrading and installing

```bash
pip install govplane-runtime-sdk
```

Python 3.11 or newer, MIT licensed, one runtime dependency (`cryptography`, needed because `ECDSA_SHA_256` cannot be verified with the standard library alone).

Read the [migration guide](https://github.com/Govplane/python-runtime-sdk/blob/main/docs/migration.md) before you upgrade. It covers both audiences: teams coming from Python 0.1.0 and teams already running the Node SDK who want the same names. Source, issues and the full changelog live at [github.com/Govplane/python-runtime-sdk](https://github.com/Govplane/python-runtime-sdk).

The test for v2 is the one stated in the docs. If Govplane went away tomorrow, could you still build, sign, distribute and evaluate your policies? For Python applications, as of 2.0, yes.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://govplane.gitbook.io/docs/documentation/resources/the-python-runtime-sdk-joins-govplane-v2.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
