Metadata-Version: 2.4
Name: axon-runtime-sdk
Version: 0.205.34
Summary: Canonical Python SDK for descriptor-bound invocation, signed receipts, and bounded streaming over Axon
Author-email: Silan Hu <silan.hu@u.nus.edu>
Maintainer-email: Silan Hu <silan.hu@u.nus.edu>
License-Expression: Apache-2.0
Project-URL: Homepage, https://easynet.run
Project-URL: Documentation, https://github.com/EasyRemote/EasyNet-Axon/tree/main/sdk/python
Project-URL: Repository, https://github.com/EasyRemote/EasyNet-Axon
Project-URL: Issues, https://github.com/EasyRemote/EasyNet-Axon/issues
Project-URL: Changelog, https://github.com/EasyRemote/EasyNet-Axon/releases
Keywords: axon-protocol,distributed-systems,remote-execution,signed-receipts,streaming
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Distributed Computing
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: NOTICE
Requires-Dist: cryptography>=41
Provides-Extra: dev
Requires-Dist: black>=24; extra == "dev"
Requires-Dist: mypy>=1.10; extra == "dev"
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: ruff>=0.5; extra == "dev"
Requires-Dist: tomli>=2; python_version < "3.11" and extra == "dev"
Dynamic: license-file

# Axon Python Runtime

`axon-runtime-sdk` is the Python distribution of the canonical Axon runtime
model; its stable import namespace is `axon_sdk`. It exposes descriptor-bound
invocation, admission, receipt proof, streaming, bidi sessions, provider
binding, and the reference `LocalRuntime`.

The protocol has one invocation shape:

```text
invoke(caller, callee, ability, subject, nonce, causal_context, args) -> receipt
```

Every public dispatch binds an exact `AbilityDescriptorRef`. The receipt closes
the same seven-field invocation with descriptor, implementation, authority,
input, output, and causal proof facts.

## Install

```bash
pip install axon-runtime-sdk
```

The runtime facade loads a target-specific Dendrite native bridge. Release
artifacts may supply that bridge as package data; source checkouts can select an
explicit bridge with:

```bash
export AXON_DENDRITE_BRIDGE_LIB=/absolute/path/libaxon_dendrite_bridge.so
```

## Descriptor-Bound Invocation

The native bridge accepts a complete request signed by caller-owned code.
Create the envelope and signature with `DescriptorBoundInvocationRequest.signed`
or bind an external signature with `DescriptorBoundInvocationDraft`; the bridge
never receives the signing key.

```python
from axon_sdk import DendriteBridge
from axon_sdk.invocation import DescriptorBoundInvocationRequest


def dispatch(bridge: DendriteBridge, request: DescriptorBoundInvocationRequest,
             request_id: str) -> dict:
    return bridge.invoke_descriptor_bound(
        request,
        request_id=request_id,
        content_type="application/json",
        timeout_ms=30000,
    )
```

The request owns caller, callee, subject, exact descriptor reference, nonce,
causal context, payload bytes and signature. Its payload must match the signed
digest. The receiving runtime verifies the caller signature and admission policy.
LocalRuntime parent handles and supervisor options are rejected rather than
silently discarded. Transport responses do not replace independent receipt
verification with trusted identity keys.

The historical session-signing shorthand (`SidecarTransport` with
`DendriteSigningConfig`, including decorator-based calls) does not match the
current native contract. Supplying signing keys to `DendriteBridge` now fails
before library loading; use the complete unary request above. The historical shorthand
remains pending migration; complete requests also support server-stream and bidi.

## Provider Binding

`AbilityDescriptor`, `AbilityImpl`, and `ProviderBinding` are generic runtime
contracts. A binding is valid only when the descriptor and implementation refer
to the same exact descriptor version.

```python
from axon_sdk import AbilityDescriptor, AbilityImpl, ProviderBinding
from axon_sdk.invocation import sha256

descriptor_ref = (
    "easynet:///r/example/ability/example.runtime.documents.summarize"
    "@descriptor.documents.v1#aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!invoke"
)

descriptor = AbilityDescriptor(
    descriptor_ref=descriptor_ref,
    descriptor_version="descriptor.documents.v1",
    schema_hash=sha256(b"documents-schema-v1"),
)
implementation = AbilityImpl(
    descriptor_ref=descriptor_ref,
    impl_hash=sha256(b"python-provider-v1"),
    runtime_env="python;provider=documents-v1",
)


async def summarize(context):
    return context.payload


binding = ProviderBinding(
    descriptor=descriptor,
    implementation=implementation,
    handler=summarize,
)
```

`LocalRuntime.bind_provider(binding)` is the canonical in-process registration
path. `LocalRuntime.register_ability(...)` constructs the same binding object
for callers that already hold the three components separately.

## Streams And Bidi

- `StreamSource`, `StreamSink`, and `LoopbackStream` provide bounded credit,
  monotonic cancellation, and one terminal frame.
- `DendriteServerStream` exposes incremental server-stream reads.
- `DendriteSignedBidiStream` carries typed stream descriptors and opaque
  content frames without interpreting downstream content.
- `EventStream` resumes invocation observation from a sequence offset.

For a complete externally signed server-stream request, use
`bridge.stream_descriptor_bound(request, request_id=..., content_type=...)`.
Unary and server-stream carriers preserve the same signed envelope and payload.
Use the returned `DendriteServerStream` as a context manager so that normal
completion, early exit and exceptions release the native handle:

```python
with bridge.stream_descriptor_bound(
    request,
    request_id="caller-owned-stream-request-id",
    content_type="application/json",
    chunk_timeout_ms=1000,
    chunk_buffer_size=64,
) as stream:
    for protocol_chunk in stream:
        consume(protocol_chunk)
```

The iterator returns native transport chunk bytes. Decoding and independent
receipt verification remain the caller's responsibility. An older library
without stream read/close capability is rejected before stream allocation.

`bridge.bidi_descriptor_bound(request, request_id=..., content_type=...,
streams=[SignedBidiStreamDescriptor(...)])` uses the same complete signed
opening, with explicit stream descriptors and bounded request/chunk buffers.
`DendriteSignedBidiStream.close()` sends EOF while allowing receipt reads;
`release()` disposes the native stream and abandons unread frames. Context exit
always releases resources, including after EOF or an exception. For graceful
completion, send EOF, drain the terminal receipt, then release.
Receipt frames preserve the native admission or terminal receipt in `receipt`
and expose `terminal`. Terminal delivery ends iteration and marks the native
handle already released; calling `release()` afterward is harmless. Receipt
projection does not verify signatures: use independently trusted identity pins.
Missing or ambiguous canonical receipt slots are rejected.

A failed or unacknowledged send leaves the sending direction in a failed state.
Later sends and EOF attempts raise the retained error without replaying the
operation. Receiving terminal evidence and releasing resources remain available.
Only acknowledged EOF is reported as idempotent success; concurrent EOF/close
calls are serialized. This does not establish whether an unacknowledged remote
operation took effect.

The v1 frame chain is not independent origin authentication. Use authenticated
transport and trusted TLS terminators as described in the
[InvokeBidi security boundary](../../document/concepts/INVOKE_BIDI_SECURITY.md).

## Runtime Process

`start_server()` connects to an existing Axon runtime or starts a local
reference runtime. Its `ServerHandle` owns an explicit process state and stops
only processes it created.

```python
from axon_sdk import start_server

with start_server() as runtime:
    print(runtime.endpoint)
```

## Verification

From this directory:

```bash
python -m pytest
python -m black --check axon_sdk tests examples
python -m ruff check axon_sdk tests examples
python -m mypy axon_sdk
python -m compileall -q axon_sdk tests examples
```

The RF-1 boundary test scans the public exports, package source, examples, and
package metadata to prevent product-owned modules or lifecycle APIs from
returning to the canonical SDK.

## Source release scope

This distribution is a deliberately bounded public SDK, not a source release
of every EasyNet control-plane service, research mechanism, or evaluation
asset. The [release-scope statement](https://github.com/EasyRemote/EasyNet-Axon/blob/main/SOURCE_RELEASE_SCOPE.md)
explains the staged policy. It does not restrict the Apache-2.0 rights granted
for files actually included in this distribution.


### Native HTTPS trust

Pass an explicit public PEM CA bundle when opening an HTTPS native session:

```python
from pathlib import Path
from axon_sdk import DendriteBridge

bridge = DendriteBridge(
    endpoint="https://runtime.example.org:50051",
    tls_ca_pem=Path("runtime-ca.pem").read_text(encoding="utf-8"),
)
```

The same `tls_ca_pem` keyword is exposed by `SidecarTransport` and the
`ability_call` decorator; their signing shorthand remains subject to the
contract limitation above. SidecarTransport preserves it when reopening the
bridge, including through its reconnect callback. The CA configures transport
trust; it is not an invocation identity, signing key or authority grant.

This requires a native bridge build supporting `tls_ca_pem` (introduced in
Axon source commit `5f739034`). Native code owns certificate parsing, the 64 KiB
limit and endpoint-hostname verification. An older bridge or invalid trust
configuration fails; the SDK does not retry without the CA or downgrade HTTPS.
No field is sent when the option is unspecified. Existing HTTP endpoints remain
unprotected; TLS termination remains a trust boundary for streamed data.

### Explicit live native TLS check

After `uv sync --extra dev` in `sdk/python`, build the current native library
from the repository root:

```sh
cargo build --locked --manifest-path core/runtime-rs/dendrite-bridge/Cargo.toml
```

Set `AXON_PYTHON_SDK_EXECUTABLE` to the absolute SDK virtual-environment Python
path and `AXON_NATIVE_BRIDGE_TEST_LIB` to the absolute library just built
(`libaxon_dendrite_bridge.dylib` on macOS, `.so` on Linux). Then run:

```sh
cargo test --locked --manifest-path core/runtime-rs/dendrite-bridge/Cargo.toml \
  common::session_tls_tests::python_native_tls_rpc_preserves_signed_request \
  -- --ignored --exact
```

This explicit test starts a bounded TLS probe, launches the real Python facade
and native library, checks the actual RPC request, and rejects wrong CA and
hostname. The receiver uses the canonical Rust signature verifier with an
independently pinned test caller public key. Changed signature, payload and
caller identity are rejected at authentication. Valid signatures reach a
deliberate policy denial; this does not prove permission to execute, replay
protection or receipt verification. It runs only when
explicitly selected because it requires the Python environment and native build.


### Explicit live runtime execution check

With the same Python and freshly built native library environment variables,
run from the repository root:

```sh
cargo test --locked --manifest-path sdk/rust/Cargo.toml --features grpc \
  --test python_native_execution python_executes_and_verifies_runtime_receipts \
  -- --ignored --exact
```

This launches Python against a real canonical Rust LocalRuntime through the
native bridge and a test-only tonic service. It checks exact binary echo output,
independently verifies admission and terminal signatures against a pinned test
callee key, checks invocation/output bindings, and rejects receipt tampering.
Unary exposes receipt endpoints rather than every lifecycle receipt, so this
check does not verify the complete intervening hash chain. The fixture
uses loopback HTTP and an explicit permissive test policy; it does not establish
production authorization, external causal ancestry, or released-package support.
The explicit prerequisites keep this test out of the default Rust test run.


### Server-stream proof boundary

`stream_descriptor_bound` currently returns raw protobuf chunks. Receiving those
bytes or verifying the terminal receipt does not verify every progress payload.
The [server-stream receipt scope](../../document/concepts/INVOKE_STREAM_RECEIPT_SCOPE.md)
records the current implementation, a reproducible substitution characterization,
and the unfulfilled protocol transcript-binding obligation. Full stream proof
verification remains pending.

## Pending event-reader cleanup

`EventStream.close()` stops observation without cancelling the provider.
When an event read is waiting, the runtime owns both the condition wait and the
close-signal wait. On either completion or cancellation of the reader task, it
cancels and joins those waits before leaving the condition context. This ordering
lets `asyncio.Condition.wait()` reacquire its lock before the context releases
it, avoiding leaked wait tasks and blocked provider completion.

`tests/test_event_reader_close.py` exercises explicit close and reader-task
cancellation with a parked provider and checks that the owned wait tasks retire.
These are single-event-loop source checks; they do not certify cross-thread
access, installed-wheel behavior or process-crash recovery.

## Complete-request Client forwarding

`Client.invoke_descriptor_bound` and `SidecarTransport.invoke_descriptor_bound`
accept the canonical caller-signed request and delegate to the native bridge.
Configure the SidecarTransport without `signing`; the caller constructs and
signs explicit executor, subject, nonce and causal facts before forwarding.

```python
with SidecarTransport(endpoint=endpoint, library_path=library_path) as transport:
    response = Client(transport).invoke_descriptor_bound(
        request, request_id="document-review",
        content_type="application/octet-stream", timeout_ms=3000,
    )
```

A bound `ability(...)` must match the signed request; `principal(...)` cannot
override its subject. Custom transports must provide the same optional method
or Client raises an explicit configuration error. Responses and native failure
receipt evidence are preserved for independent verification. This method does
not retry invocations. Concurrent initialization/reconnect and other lifecycle
surfaces require separate acceptance; this unary evidence does not prove them.

The existing ability/payload `call`/`call_raw` paths still require session signing
and infer callee from ability. They are **unsupported native signed-call paths**
with the current native schema. An ability URI does not identify its executor.
Use the complete request above or the direct bridge entrypoint. See the
[native entrypoint matrix](../conformance/native-entrypoint-evidence.md).

The explicit `python_exchanges_complete_signed_bidi_through_native` Rust
integration exercises an installed local Python wheel against the canonical
loopback bidi service and a pinned native library. It checks one binary input
plus EOF, actual progress/output, invalid-signature and replay rejection, and
independently pinned admission and success/failure terminal receipts, including
nonce/output binding and tamper rejection. Unary bridge/Client regression passes
alongside it. A subsequent [coordinated candidate run](../conformance/native-entrypoint-evidence.md#coordinated-candidate-with-python-bidi-2026-09-07)
also passed all eight explicit native selections, including Python bidi.
These local results do not establish registry installation, TLS deployment or
complete transcript proof.

`python_streams_complete_signed_requests_through_native` additionally exercises
the installed wheel's complete-request server stream. Python returns exact raw
protobuf chunks to Rust's shared verifier, which checks progress bytes, order,
success/failure terminal output and independently pinned endpoint receipt
signatures with identity, nonce and output bindings. Wrong signer/replay are
rejected. The candidate gate now requires this exact test alongside Python
unary/bidi. The rebuilt [nine-selection candidate run](../conformance/native-entrypoint-evidence.md#coordinated-candidate-with-python-server-stream-2026-09-07)
also passed, including this exact stream test.
Endpoint receipts do not establish a complete signed stream transcript.
