Metadata-Version: 2.4
Name: reva-ai-authz
Version: 1.0.3
Summary: Reva AI agent authorization, proxy, and observability SDK (FastAPI, LangChain, LangGraph, CrewAI, AutoGen, MCP)
Author-email: "Reva AI, Inc." <engineering@reva.ai>
Maintainer-email: "Reva AI, Inc." <engineering@reva.ai>
License-Expression: LicenseRef-Proprietary
Project-URL: Homepage, https://reva.ai
Project-URL: Source Code, https://gitlab.com/reva-ai
Keywords: authorization,policy,pdp,cedar,ai-security,agents,nhi,guardrails,rtg,mcp,langchain,langgraph
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Framework :: FastAPI
Requires-Python: <3.14,>=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastapi>=0.109
Requires-Dist: httpx>=0.25
Requires-Dist: pydantic>=2.5; python_version < "3.13"
Requires-Dist: pydantic>=2.9.2; python_version >= "3.13"
Dynamic: license-file

# Reva AI Authorization SDK

`reva-ai-authz` protects inbound agent/tool boundaries and outbound
Agent, API, and MCP hops with Reva Trust Gateway (RTG/PDP) authorization.
It also carries authenticated caller identity, W3C tracing, and sanitized
multi-hop intent history across supported integrations.

## Release state

| Item | Version |
|---|---|
| Version described by this source tree | `1.0.3` |
| Public PyPI baseline last verified on 2026-08-12 | `1.0.2`; `1.0.3` was not published at that check |
| Authoritative current state | installed package metadata and the public PyPI release files |

This packaged README is immutable release documentation, not a live package
index. Check [the public PyPI project](https://pypi.org/project/reva-ai-authz/)
before changing a production pin. If the reviewed 1.0.3 wheel and sdist are not
both published and public-index verification has not passed, remain on the
verified baseline:

```bash
python3.10 -m pip install "reva-ai-authz==1.0.2"
```

The intended annotated tag is lowercase `v1.0.3` with message
`Release reva-ai-authz 1.0.3`. Creating that tag builds a candidate; it does
not publish until an authorized maintainer runs the protected manual job.

To test the 1.0.3 candidate, install the reviewed wheel artifact from the tag
pipeline, not a similarly named file from an ad hoc build:

```bash
python3.10 -m pip install ./reva_ai_authz-1.0.3-py3-none-any.whl
python3.10 -c 'from importlib.metadata import version; import reva_ai; assert version("reva-ai-authz") == reva_ai.__version__ == "1.0.3"'
```

Once PyPI shows the reviewed 1.0.3 wheel and sdist and public-index verification
passes, install with:

```bash
python3.10 -m pip install "reva-ai-authz==1.0.3"
```

Python 3.10–3.13 are supported by the 1.0.3 CI matrix. Framework packages such
as LangChain, LangGraph, CrewAI, AutoGen, and MCP are application dependencies;
this distribution does not install them.

### Migrating from 1.0.2

1. Deploy the coordinated 1.0.3 PDP and RTG evaluator changes before changing
   the customer wheel. An older PDP ignores `rtg/a2a.context` and cannot carry
   it between Issue Token and Validate Token.
2. Audit every `proxy.mcp(...)` call. Protection now defaults on; use
   `endpoint_protected=False` only for an intentionally local, non-policy tool.
3. Restrict direct `cedar_context` and every value resolved by
   `cedar_context_map` to exact schema keys with string, boolean, safe integer,
   or homogeneous scalar-list values. Null, float, record, nested, and
   mixed-list values fail validation. Prefer the declarative map for values in
   the current schema-validated business payload. Keep the encoded mapping
   within 512 bytes; transaction JWEs over 4,096 bytes and complete PDP header
   carriers over 6,144 bytes fail closed. Complete compact PDP JSON is limited
   to 1,048,576 bytes; keep PDP
   `PDP_AI_REQUEST_BODY_MAX_BYTES=1048576` to match that fixed SDK preflight.
   History is rejected—not truncated—above 65,536 bytes, depth 32, 32 logical
   events, 2,048 JSON values, or 16,384 UTF-8 bytes in one string.
4. Handle `PDPRequestError`, `PDPUnavailableError`, and `PDPProtocolError`
   separately from a definitive `RevaAuthorizationError` deny.
5. Use HTTPS for `RTG_URL`. Plain HTTP requires the explicit local/private/
   in-cluster development escape hatch described below.
6. Final-turn recording now requires an explicit FastAPI-route opt-in and an
   approved same-RTG-origin HTTPS endpoint. The old
   `mock_rtg_record_final_turn` import remains temporarily compatible but only
   emits a deprecation warning and transmits nothing.
7. Replace direct context/history mutation with the documented decorators,
   proxies, adapters, and one isolated `standalone_work_item` per authenticated
   worker message.

Do not change a production pin until the candidate wheel and the coordinated
PDP/RTG deployment pass the acceptance checklist. The presence of this
migration guide is not evidence of publication; use the public index and
installed package metadata as the authority.

## What must exist before code integration

The SDK cannot invent control-plane metadata. Register and test all of the
following in the customer's policy store before running these examples:

- the exact `agent_name` used by each inbound route;
- every destination Agent, API, MCP server, and tool resource ID;
- each source-to-destination topology edge and action (`invokeAgent`,
  `invokeTool`, `read`, `write`, or `delete`);
- the static destination URL and endpoint type used for resource resolution;
- every Cedar context attribute used by policy, with identical spelling and
  type; and
- the resource skill catalog used by PDP's `promptIntent` matcher.

An ID mismatch is expected to deny. A Cedar policy on `context.HSNewApp_mrn`
does not match a payload field named `mrn` unless the application maps it with
`cedar_context={"HSNewApp_mrn": value}` or declaratively with
`cedar_context_map={"HSNewApp_mrn": "mrn"}`.

SDK 1.0.3's managed context and intent-consistency guarantees require the
coordinated 1.0.3 PDP and RTG evaluator changes. An older PDP parses the request
but ignores `rtg/a2a.context`; an older evaluator may accept `ALIGNED` for a
proven action change. Record the deployed SDK wheel hash plus PDP, RTG, gateway,
policy-schema, and guardrail-template versions during customer acceptance.

## Environment

Set configuration before importing `reva_ai` or defining protected routes:

```bash
export RTG_URL="https://rtg.customer.example"
export POLICYSTORE_ID="customer-policy-store-id"

# Optional platform credential, injected from the customer's secret manager.
export RTG_AUTH_TOKEN="..."

# Logs may contain business data. Use a writable protected path and a safe level.
export REVA_AI_LOG_FILE="/var/log/customer-agent/reva-ai-sdk.log"
export REVA_AI_LOG_LEVEL="WARNING"
export REVA_AI_LOG_STDOUT="false"

# PDP may spend up to five minutes on authoritative guardrail retries.
export REVA_AI_PDP_TIMEOUT_SECONDS="330"
```

The SDK does not create a file unless `REVA_AI_LOG_FILE` is explicitly set.
If an explicitly configured file cannot be opened, file logging is disabled
with a warning rather than failing package import. Application logging handlers
or `REVA_AI_LOG_STDOUT=true` can be used in read-only containers.

Do not set `RTG_DISABLED` or `REVA_AI_ALLOW_EXTERNAL_CONTEXT_WRITES` in a
protected environment. `RTG_DISABLED` is an explicit local-test bypass.

`RTG_URL` must be an absolute HTTPS base URL without embedded credentials,
query parameters, fragments, or path-traversal segments. A base path is
allowed. For local/private/in-cluster development only, plain HTTP also
requires `REVA_AI_ALLOW_INSECURE_RTG_HTTP=1` and emits a warning. Keep that
escape hatch unset in production; it does not apply to final-turn delivery.

The SDK sends `RTG_AUTH_TOKEN` to RTG/PDP as `X-API-Token`; it does not replace
the end-user access credential or transaction token. Never put either token in
a prompt, business body, `cedar_context`, history, log field, or model-visible
tool argument.

`REVA_AI_PDP_TIMEOUT_SECONDS` is the timeout for PDP authorization calls only;
it defaults to 330 seconds and accepts finite values from 1 to 900. The default
exceeds PDP's five-minute total guardrail budget by 30 seconds. Keep gateway,
ingress, load-balancer, and application request deadlines above this value, or
reduce the coordinated PDP retry budget. Downstream agent/API/MCP transport
timeouts remain separate.

If the application uses `reva_ai_discover`, treat it as optional startup
telemetry: it stores application metadata and performs a synchronous
`/discovery-logs` POST. It does not provide missing `RTG_URL` or
`POLICYSTORE_ID` to authorization; protected paths read those values from the
environment and fail closed when either is absent.

## Minimal FastAPI boundary

Use a strict object schema, validate a nonblank prompt, and decorate the route
inside the normal FastAPI decorator:

```python
from typing import Annotated

from fastapi import FastAPI, Request
from pydantic import BaseModel, ConfigDict, StringConstraints

from reva_ai.sdk.ai import reva_ai_authorise


NonBlank = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]


class InvokeBody(BaseModel):
    model_config = ConfigDict(extra="forbid")

    message: NonBlank
    conversation_id: NonBlank


app = FastAPI()


@app.post("/invoke")
@reva_ai_authorise(
    agent_name="support-agent",
    action="invokeAgent",
    auth_key="Authorization",
    prompt_key="message",
    principal_claim="sub",
    cedar_context_map={"SupportApp_conversationId": "conversation_id"},
)
async def invoke(body: InvokeBody, request: Request) -> dict[str, str]:
    # Customer code runs only after PDP allows.
    return {"answer": f"Accepted {body.conversation_id}"}
```

Pass exactly one of `agent_name`, `tool_name`, or `crew_name`. The body key
`rtg/a2a` is SDK-owned and is removed before a strict downstream handler runs.
Do not catch `RevaAuthorizationError` and continue protected work.

The whole-app middleware helper intentionally skips `GET`, `HEAD`, and
`OPTIONS`. Decorate sensitive read routes directly; do not claim that the
middleware protects them.

## Agent-to-agent HTTP

Call the proxy inside a decorated request or an isolated worker context. URLs
must come from static allowlisted configuration, never unchecked user/model
output:

```python
from reva_ai.sdk.ai.proxy import proxy


async def call_fulfilment(message: str, order_id: str) -> dict:
    response = await proxy.agent(
        endpoint_url="https://fulfilment-agent.internal.example/invoke",
        method="POST",
        body={"message": message, "order_id": order_id},
        prompt_key="message",
        resource_id="fulfilment-agent",
        cedar_context_map={"OrderApp_orderId": "order_id"},
    )
    response.raise_for_status()
    return response.json()
```

`cedar_context` and `cedar_context_map` are PDP-only assertion channels. The
SDK places the combined validated result under managed `rtg/a2a.context` for
authorization, but does not add the Cedar target aliases to the downstream
Agent/API/MCP business payload or customer handler arguments. Existing source
business fields remain unchanged.

Use `cedar_context={"Cedar_key": value}` when the application already holds a
validated value. Use `cedar_context_map={"Cedar_key": "business.path"}` to
resolve a value from the current invocation on every call. The latter is
available on validated inbound `@reva_ai_authorise` route/tool boundaries,
outbound Agent/API/protected-MCP proxies, inner-call helpers, and supported
framework adapters. A path is case-sensitive and may be an exact top-level key,
a dotted path such as `patient.mrn`, or an indexed path such as
`items[0].code`:

```python
cedar_context_map = {
    "HSNewApp_mrn": "patient.mrn",
    "HSNewApp_primaryCode": "items[0].code",
}
```

The mapper is declarative; it does not execute a callback. A missing path, a
resolved null, a target also supplied in `cedar_context`, or a path selecting a
credential-shaped segment, `history`, `onBehalfOf`, `promptIntent`, or
`rtg/a2a` raises `ValidationError` before PDP. The combined direct and mapped
result then uses the limits below.

The mapping accepts at most 64 attributes and 512 bytes of encoded UTF-8 JSON.
Keys must be nonblank strings of at most 128 characters. Values must be a
string, boolean, integer in `[-(2**53 - 1), 2**53 - 1]`, or a list/tuple whose
items all have one of those same scalar types. Empty lists are accepted. Null,
every float (including `1.0`), records/objects, nested collections, mixed-type
lists, unsafe integers, identity fields such as `onBehalfOf`, `promptIntent`,
`subject`, or `principal`, routing/prompt/history fields, credential-like keys,
credential-shaped values, and non-JSON values are rejected.

Explicit/resolved context wins over a same-named root business field at Issue
Token. On allow, PDP binds only that validated managed mapping—not arbitrary
business fields—into the encrypted transaction token. At the receiving
Validate Token PEP, PDP combines the new business body with the authenticated
token mapping;
a colliding body field or caller-written `rtg/a2a.context` cannot replace the
token-bound value. A later authorized Issue Token call may supply a new mapping
for its replacement token; if it supplies none, the prior authenticated mapping
is preserved. This two-PEP behavior requires the coordinated 1.0.3 PDP and is
not supplied by the published 1.0.2 wheel alone.

Treat these values as request assertions, not proof of user identity or role.
The route decorator maps only the FastAPI/Pydantic-validated value, and the MCP
tool decorator maps the value actually passed to the tool; raw transport JSON
cannot supply a different policy value. `reva_ai_authorise_request` and
whole-app middleware run before route-model validation, so they reject nonempty
`cedar_context_map`. The sub-app compatibility decorator exposes only fixed
`cedar_context`. Use fixed context on those boundaries or a direct route
decorator for dynamic mapping:

```python
validated_mrn = body.mrn  # validated by the application schema
cedar_context = {"HSNewApp_mrn": validated_mrn}
```

Never copy arbitrary model output into `cedar_context`, and never derive a
`cedar_context_map` from caller/model input.

The PDP endpoints have related but non-identical contracts:

| Endpoint | Context shape | Role |
|---|---|---|
| `POST /pdp/access/v1/token/enrich` | managed `rtg/a2a.context` | Outbound proxies authorize Issue Token and mint the encrypted transaction token that carries the explicit mapping. |
| `POST /pdp/access/v1/agent/evaluation` | managed `rtg/a2a.context`; authenticated token claim wins during transaction-token validation | Inbound decorators and inner-call adapters authorize Validate Token/logical invocations. |
| `POST /pdp/access/v1/ai/evaluation` | top-level `context` | Runs the same strict managed-context, principal-last, history, Cedar/AVP, and guardrail rules over the direct body shape. It does not mint the proxy transaction token and is not an identical fallback or a second opinion after deny. |

For direct evaluation, exact `context.chatHistory`, `context.conversation`, and
`context.hops` members are separated as guardrail-history metadata before the
remaining `context` members are validated as managed Cedar attributes. This
keeps the existing direct-client history shapes working without admitting
nested records through the strict Cedar-value channel. A direct
`principal.type` and `principal.id` become the same authoritative typed
`onBehalfOf` entity used by Cedar/AVP and guardrails.

All three endpoints reject a complete compact UTF-8 JSON body larger than
1,048,576 bytes before authorization, audit, or guardrail work. SDK-managed
calls measure and send the exact same byte sequence. A direct client should do
the same, as below; chunked transfer and omitted `Content-Length` do not bypass
the PDP limit. Keep PDP `PDP_AI_REQUEST_BODY_MAX_BYTES=1048576` for exact SDK
parity. PDP permits an explicitly configured value from 131,072 through
8,388,608 bytes, but changing it is a deployment-contract change: a lower
value can reject an SDK-preflighted request and a higher value does not raise
the SDK's fixed 1 MiB limit. PDP independently caps the canonical JSON sent to
an external guardrail endpoint at 262,144 bytes before the first HTTP attempt.
Oversized authoritative evaluator input fails closed and is never truncated or
retried; an informational endpoint records the failure without becoming
authority.

Call direct AI evaluation only through the authenticated gateway. Pass secrets
and tenant configuration from a trusted secret/config provider:

```python
import json
from typing import Any
from urllib.parse import urlsplit

import httpx


async def evaluate_direct_ai(
    *,
    gateway_url: str,
    api_token: str,
    policy_store_id: str,
    prompt: str,
    mrn: str,
    caller_agent_id: str,
    principal_user_id: str,
    audit_authorization: str | None = None,
) -> dict[str, Any]:
    gateway_url = gateway_url.strip().rstrip("/")
    api_token = api_token.strip()
    policy_store_id = policy_store_id.strip()
    prompt = prompt.strip()
    mrn = mrn.strip()
    caller_agent_id = caller_agent_id.strip()
    principal_user_id = principal_user_id.strip()
    parsed_gateway = urlsplit(gateway_url)
    if parsed_gateway.scheme != "https" or not parsed_gateway.netloc:
        raise ValueError("gateway_url must use HTTPS")
    if not all(
        (api_token, policy_store_id, prompt, mrn, caller_agent_id, principal_user_id)
    ):
        raise ValueError("all direct AI evaluation inputs are required")

    headers = {
        "Content-Type": "application/json",
        "X-API-Token": api_token,
        "policyStoreId": policy_store_id,
    }
    if audit_authorization and audit_authorization.strip():
        headers["Authorization"] = audit_authorization.strip()

    body = {
        "subject": {"type": "Agent", "id": caller_agent_id},
        "action": {"name": "invokeAgent"},
        "resource": {"type": "Agent", "id": "records-review-agent"},
        "principal": {"type": "User", "id": principal_user_id},
        "context": {"HSNewApp_mrn": mrn},
        "transmission": {
            "promptKey": "content",
            "role": "user",
            "content": prompt,
        },
    }
    payload_bytes = json.dumps(
        body,
        ensure_ascii=False,
        separators=(",", ":"),
        allow_nan=False,
    ).encode("utf-8")
    if len(payload_bytes) > 1_048_576:
        raise ValueError("direct AI evaluation body exceeds 1 MiB")

    # Must exceed PDP's five-minute authoritative guardrail budget.
    async with httpx.AsyncClient(timeout=330.0) as client:
        response = await client.post(
            f"{gateway_url}/pdp/access/v1/ai/evaluation",
            headers=headers,
            content=payload_bytes,
        )
    if response.status_code == 403:
        raise PermissionError("PDP denied direct AI evaluation")
    response.raise_for_status()
    result = response.json()
    if result.get("decision") is not True:
        raise PermissionError("PDP denied direct AI evaluation")
    return result
```

`subject` is the asserted calling workload, while `principal` is the asserted
originating user/on-behalf-of identity. Today the direct endpoint authenticates
the gateway request with mandatory `X-API-Token`, but it does **not** itself
cryptographically bind body `subject`, body `principal`, or `policyStoreId` to
that credential. Treat `/ai/evaluation` as a trusted-service assertion API:
never expose it to an untrusted caller, never populate those fields from raw
request/model input, scope the service credential to the tenant, and source
the identities and policy-store ID only from authenticated service/user context
and fixed trusted configuration. They must also match registered policy
metadata. An optional `Authorization` value is captured only as sanitized
decision-audit context; it does not authenticate this endpoint or affect the
decision. Do not send `promptIntent`: it is ignored as caller input and remains
server-owned. A deny ends the operation. Direct evaluation neither mints an
`X-Transaction-Token` nor permits `/token/enrich` or another endpoint as
fallback after denial.

For protected MCP, acceptance testing must prove the attribute at both calls:
the client-side `/token/enrich` Issue Token decision and the server decorator's
`/agent/evaluation` Validate Token decision.

## Trusted external API

`proxy.api` keeps the downstream body unchanged, maps HTTP methods to
`read`/`write`/`delete`, and sends the newly issued transaction token to the
target. Use it only with a registered, trusted HTTPS service that is permitted
to receive that token:

```python
from reva_ai.sdk.ai.proxy import proxy


async def payment_status(payment_id: str) -> dict:
    response = await proxy.api(
        endpoint_url="https://payments-api.internal.example/v1/status-lookups",
        method="POST",
        endpoint_type="Api",
        body={"payment_id": payment_id},
        prompt_key="payment_id",
        cedar_context={"Payments_paymentId": payment_id},
    )
    response.raise_for_status()
    return response.json()
```

Do not use this helper to send Reva transaction credentials to a public SaaS
endpoint. Query parameters are sent downstream but are not part of the current
PDP business body; place authorization-relevant fields in the object body or
explicit `cedar_context`.

## Protected MCP client

MCP proxying is protected by default in 1.0.3. Supply the exact registered MCP
resource ID; the JSON-RPC tool name and the policy resource ID are not assumed
to be interchangeable:

```python
from reva_ai.sdk.ai.proxy import proxy


async def get_labs(mrn: str):
    return await proxy.mcp(
        tool="get_lab_results_lg",
        arguments={
            "message": f"Review the latest lab results for {mrn}",
            "mrn": mrn,
        },
        mcp_client="https://healthsphere-mcp.internal.example/mcp",
        resource_id="healthsphere-mcp_lg/get_lab_results_lg",
        transport="streamable_http",
        cedar_context_map={"HSNewApp_mrn": "mrn"},
    )
```

The agent-to-tool operation makes two security-relevant calls:

1. Agent → PDP `POST /pdp/access/v1/token/enrich` (`invokeTool` / MCP), where
   Cedar and synchronous guardrails run and a transaction token is issued.
2. Agent → MCP server `tools/call`, where the server's
   `@reva_ai_authorise(tool_name=...)` validates the token.

`rtg/a2a` is the name of the managed metadata key shared by Agent and MCP
envelopes; it is not the Agent-to-Agent URL path.

Managed context is capped at 512 bytes before encryption so its JWE/base64url
expansion plus existing claims stays within deployed HTTP header budgets. PDP
and the SDK fail closed if a newly issued compact transaction JWE exceeds 4,096
UTF-8 bytes. The SDK also rejects the complete PDP request-header set before
network I/O when its serialized size exceeds 6,144 bytes. Reduce bounded policy
context or metadata; never truncate a credential or bypass authorization to fit
it.

Credentials remain HTTP transport headers and are not mirrored into
model-visible `params.arguments`. Protected stdio is therefore rejected while
RTG is enabled because it has no reviewed transport-isolated credential
channel. Use streamable HTTPS for protected MCP. An intentionally unprotected
local tool requires the visible opt-out `endpoint_protected=False`.

Unknown `proxy.mcp` keyword arguments raise `TypeError`; misspelled security
options no longer disappear silently.

## Protected MCP server

The server decorator uses the physical MCP HTTP transport credential. Customer
tool functions should expose only business arguments:

```python
from mcp.server.fastmcp import FastMCP

from reva_ai.sdk.ai import reva_ai_authorise


mcp = FastMCP("healthsphere-mcp")


@mcp.tool()
@reva_ai_authorise(
    tool_name="get_lab_results_lg",
    action="invokeTool",
    prompt_key="message",
    principal_claim="sub",
)
async def get_lab_results_lg(message: str, mrn: str) -> dict:
    return {"mrn": mrn, "results": []}
```

Verify the exact FastMCP version exposes the current physical HTTP request to
the decorator. Test `tools/call`; a successful `initialize` or `tools/list`
does not prove tool authorization.

## Standalone workers

Use one context per work item. The context manager clears identity, credentials,
history, and tracing even if customer code raises.

A true first-hop queue message may establish one sanitized originating prompt:

```python
from reva_ai.sdk.ai.adapters import (
    authorise_inner_call_async,
    standalone_work_item,
)


async def process_first_hop(message) -> None:
    payload = message.json()
    with standalone_work_item(
        agent_name="batch-supervisor-agent",
        agent_type="Agent",
        auth_token=message.headers["Authorization"],
        incoming_headers=message.headers,
        principal_claim="sub",
        originating_payload=payload,
        originating_prompt_key="message",
    ):
        await authorise_inner_call_async(
            resource_name="billing-agent",
            resource_type="Agent",
            action="invokeAgent",
            current_payload=payload,
            prompt_key="message",
            cedar_context={"Billing_accountId": payload["account_id"]},
        )
        await process_authorized_payload(payload)
```

`originating_payload` creates only a minimal root prompt hop. It does not import
caller-supplied raw history, identity, or credentials. It requires a fresh
context and cannot be combined with a transaction token.

A delegated queue message must carry the authenticated managed envelope instead:

```python
from reva_ai.sdk.ai.adapters import (
    authorise_inner_call_async,
    standalone_work_item,
)


async def process_delegated_message(message) -> None:
    payload = message.json()
    with standalone_work_item(
        agent_name="batch-supervisor-agent",
        agent_type="Agent",
        auth_token=message.headers["X-Transaction-Token"],
        incoming_headers=message.headers,
        inbound_rtg_a2a=payload["rtg/a2a"],
    ):
        await authorise_inner_call_async(
            resource_name="batch-supervisor-agent",
            resource_type="Agent",
            action="invokeAgent",
            current_payload=payload,
            prompt_key="message",
        )
        await process_delegated_payload(payload)
```

`standalone_work_item` only isolates and seeds SDK context. It does not
authenticate the queue message, verify broker provenance, or authorize
business work. The consumer must authenticate/trust the ingress transport,
keep the managed envelope bound to its transaction token, and obtain an allow
from PDP before any business side effect.

Applications must not call internal history setters or enable external context
writes to manufacture a more favorable guardrail baseline.

## Framework adapters

Adapters require a decorated outer request or `standalone_work_item`.

LangChain:

```python
from reva_ai.sdk.ai.adapters.langchain import protect_runnable, protect_tool

protected_chain = protect_runnable(
    chain,
    agent_name="summarizer-agent",
    prompt_key="message",
)
protected_tool = protect_tool(
    account_tool,
    tool_name="account-mcp/lookup-account",
    resource_type="MCP",
    prompt_key="account_id",
)
```

`batch` and `abatch` authorize every item before executing any item.

LangGraph:

```python
from reva_ai.sdk.ai.adapters.langgraph import protect_node

@protect_node(agent_name="planner-agent", prompt_key="message")
async def planner_node(state: dict, config: dict) -> dict:
    return await plan(state)
```

AutoGen:

```python
from reva_ai.sdk.ai.adapters.autogen import protect_agent

protected_assistant = protect_agent(
    assistant,
    agent_name="research-assistant-agent",
    prompt_key="content",
)
```

CrewAI factories must retain their return annotations and perform construction
only. Keep network calls, writes, and tool execution out of a factory body.

## Errors and fallback behavior

Import exceptions from `reva_ai.sdk.ai.utils`:

```python
from reva_ai.sdk.ai.utils import (
    ConfigurationError,
    HistoryValidationError,
    PDPProtocolError,
    PDPRequestBodyTooLargeError,
    PDPRequestError,
    PDPUnavailableError,
    RevaAuthorizationError,
)
```

| Exception | Meaning | Retry/fallback guidance |
|---|---|---|
| `RevaAuthorizationError` | PDP returned 401/403 or an explicit deny | Do not retry through another authorization path |
| `PDPRequestError` | Non-denial 4xx; request/config contract is invalid | Fix the request or deployment configuration |
| `PDPUnavailableError` | Timeout, network failure, 408/425/429, or 5xx | Protected work stays stopped; bounded infrastructure retry only for an idempotent authorization call |
| `PDPProtocolError` | Successful HTTP response is malformed or missing token/decision | Stop; SDK/PDP versions are incompatible or PDP is faulty |
| `HistoryValidationError` | Security history is malformed or exceeds the shared history limits | Safe HTTP 400; fix the input; do not truncate history |
| `PDPRequestBodyTooLargeError` | Compact SDK-to-PDP JSON exceeds 1,048,576 bytes | Safe HTTP 413 before network access; reduce the payload |
| `ConfigurationError` | Required local SDK configuration is missing | Fix startup configuration |

FastAPI's default response detail is safe-by-default: it omits raw PDP bodies,
policy reasons, and internal error codes. The exception object deliberately
retains `reason`, `code`, `pdp_response`, and (where applicable)
`upstream_status_code` for trusted diagnostics. Do not serialize the exception
object or those fields into customer responses or ordinary logs. After PDP
allows and execution enters the customer handler, the decorator re-raises the
application's exception unchanged so existing FastAPI exception handlers keep
their normal semantics.

Fallback to an alternate endpoint is permitted only for
`PDPUnavailableError`, when Reva has explicitly defined that endpoint as the
same authorization authority. Never catch `RevaAuthorizationError` and ask a
different path for a second opinion.

The SDK does not retry business side effects automatically. A lost downstream
response is not evidence that the operation did not happen.

## Intent history, `promptIntent`, and guardrail authority

These concepts are intentionally separate:

- `rtg/a2a.history` is the sanitized recursive hop chain. The RTG evaluator
  selects the deepest prompt-bearing history node as originating intent and
  compares it with the current hop.
- `context.promptIntent` is generated by PDP from the current prompt and the
  destination resource's registered skill catalog. It is a list of matched
  Skill entity references, not originating natural language. The SDK never
  sets it. An empty list can mean no usable skills/match or matcher failure; it
  does not prove that history was lost.
- Exactly one guardrail endpoint is the synchronous `decisionMaker`. In the
  standard split IBAC template, `/ibac/score` is authoritative, while
  `/ibac/intent` and `/ibac/drift` are post-decision informational endpoints.
  Changing a rule to ENFORCE does not make an informational `BLOCK` response
  authoritative.

SDK 1.0.3 and the coordinated PDP reject history that exceeds 65,536 compact
UTF-8 JSON bytes, depth 32, 32 logical events, 2,048 total JSON values, or
16,384 UTF-8 bytes in one string. A linked history node and each
`chatHistory` item count once; `conversation.messages` and `hops` are unioned,
with entries sharing the same numeric `seq` counted once. The `rtg/a2a`
metadata block and promptless conversation/hops compatibility wrappers do not
count independently. A canonical linked wrapper retained by the guardrail
builder does count. Cycles, non-JSON values, and non-finite
numbers also fail with a safe HTTP 400. Security history is never truncated to
pass authorization.

The 1.0.3 RTG evaluator applies a server-owned `0.85` deny floor when explicit
operation-family or polarity replacement is proven over the same business
object. The regression case is “review MRN-1004 lab results” followed by
“place Furosemide order for MRN-1004”; `/score` can no longer accept
`ALIGNED / 0.05` for that canonical input.

## Optional final-turn recording

Conversation metadata alone does not send content. Recording is disabled by
default and requires all of the following:

- `record_final_turn=True` on a FastAPI route decorator;
- `conversation_key` and `conversation_key_location` (`header` or `body`);
- an explicit absolute HTTPS `final_turn_url`, or
  `REVA_AI_FINAL_TURN_URL`; and
- customer approval for payload content, retention, residency, tenancy, and
  failure handling.

The endpoint must use exactly the same HTTPS scheme, host, and port as
`RTG_URL`, with no embedded credentials, query, or fragment, because the POST
carries `RTG_AUTH_TOKEN`. The insecure RTG development escape hatch does not
apply. Recording runs only for an edge request without
`X-Transaction-Token`; chained-agent responses are not recorded by this path.

```python
@app.post("/chat")
@reva_ai_authorise(
    agent_name="support-agent",
    prompt_key="message",
    conversation_key="conversation_id",
    conversation_key_location="body",
    record_final_turn=True,
    final_turn_url="https://rtg.customer.example/v1/final-turn",
)
async def chat(body: InvokeBody, request: Request) -> dict:
    return {"answer": "..."}
```

The POST is background-dispatched and does not delay a non-streaming response.
Delivery is best effort: it has a 10-second HTTP timeout, no retry/durable
queue/shutdown drain, and a maximum of 100 concurrent recording tasks; excess
turns are dropped. Responses are capped at 50,000 characters (the rolling tail
for a stream). Recording failures never turn an allowed business response into
an error. The recorder adds no dedicated prompt/response log, but optional
TRACE logging elsewhere remains payload-capable. There is no derived
`/mock/...` endpoint in 1.0.3. The deprecated
`mock_rtg_record_final_turn` helper remains importable as a warning-emitting,
non-transmitting no-op.

## Security boundary and current limitations

- **Gateway verification is mandatory.** The SDK can inspect access-token
  claims without locally verifying their signature. The API gateway must use an
  algorithm allowlist and trusted JWKS, validate token use/type, issuer,
  audience/client, `exp`, and `nbf`, strip forbidden caller metadata, and block
  direct workload/PDP bypass. A shared `RTG_AUTH_TOKEN` alone is not proof of
  the asserted workload identity.
- **Outbound headers require trust-domain review.** Proxy construction starts
  from a broad inbound header snapshot. Until strict per-target allowlists are
  implemented, strip unrelated cookies, proxy authorization, CSRF values, and
  customer secrets at ingress; call only approved targets and assert the exact
  destination headers in tests. The SDK strips its RTG platform token and
  replaces session credentials, but that is not a general header allowlist.
- **Destination URL allowlisting is application-owned.** Public proxy helpers
  do not enforce a complete HTTPS host allowlist or prevent DNS rebinding.
  Endpoints must be static trusted configuration, never user/model output;
  review environment-proxy behavior as well.
- **Some async proxy paths block for PDP.** Agent/API/MCP async calls perform
  synchronous token enrichment before downstream async I/O. Load-test event
  loop latency and use bounded concurrency.
- **Logs are sensitive.** The SDK eagerly creates a non-rotating file handler.
  INFO can include service/action/resource/tool names, paths, and URLs; explicit
  TRACE can additionally include redacted business bodies, prompts, history,
  arguments, and responses. Use WARNING in production where possible, protect
  the file, rotate/retain it deliberately, and do not treat heuristic redaction
  as removal of all customer data.
- **Framework boundaries are specific.** Whole-app middleware skips
  `GET`/`HEAD`/`OPTIONS`. Protected MCP stdio is rejected; prefer static
  streamable HTTPS. Legacy SSE requires live same-origin/security testing, and
  WebSocket is unsupported. Slow MCP initialization can consume encrypted
  transaction-token TTL; PDP, not the SDK's local three-part-JWT check, is
  authoritative for the five-part JWE expiry.
- **Context does not authorize detached work.** A detached task can inherit
  `ContextVar` identity, token, and history. Enqueue sanitized data and start a
  fresh authenticated `standalone_work_item` rather than launching detached
  protected business work from a request.
- **CrewAI factories execute before their PDP check.** Keep construction pure:
  no network call, write, or tool side effect in a factory body. Keep required
  return annotations and test against the pinned framework version.
- **Guardrail outage behavior is server policy.** Verify the effective
  synchronous `decisionMaker` and its failure mode for every ENFORCE template;
  SDK exception names do not prove that evaluator outage is fail closed.
- **The optional top-level `ai` name is not an invocation API.**
  `reva_ai.sdk.ai.ai` may be `None`; use the documented decorators, proxies,
  and adapters.

No SDK can guarantee an error-free or universally secure deployment. Customer
readiness requires the actual gateway, broker, policy store, PDP/RTG versions,
framework versions, endpoints, logs, and data-handling controls to pass the
acceptance tests below.

## Acceptance checklist

Before customer handoff, prove in the deployed environment:

1. Missing/invalid credentials deny before customer code runs.
2. Cedar allow and deny policies work for exact registered IDs and schema keys.
   For direct `/ai/evaluation`, prove the gateway/service boundary prevents an
   untrusted caller from selecting `subject`, `principal`, or `policyStoreId`,
   and that the API credential is tenant-scoped. The endpoint does not perform
   that binding itself.
3. Direct `cedar_context` and nested/indexed `cedar_context_map` values reach
   Issue Token, win over same-name business keys, cannot override server-owned
   identity/intent, remain absent from the downstream body, and reach Validate
   Token through the authenticated transaction-token claim; missing, null,
   protected, or colliding mapper configuration fails before PDP, and a forged
   downstream body value cannot replace token-bound context. Exact and
   over-limit checks pass at 512-byte context, 4,096-byte compact-token, and
   6,144-byte complete-PDP-header boundaries; all three PDP body shapes pass at
   the exact 1,048,576-byte compact UTF-8 boundary and fail above it; canonical
   external-guardrail JSON passes at 262,144 bytes and fails above it before
   any HTTP attempt. Keep PDP `PDP_AI_REQUEST_BODY_MAX_BYTES=1048576` so its
   configured streamed-body limit matches the SDK's fixed preflight.
4. Agent → Agent and Agent → MCP chains preserve principal, current prompt,
   originating history, canonical resource ID, thread ID, and trace ID; exact
   32-event history passes and 33 events, excessive depth/value/string/byte
   counts, cycles, and non-finite data fail without truncation.
5. A PDP deny never triggers a fallback call; an unavailable failure is typed
   separately.
6. Protected MCP is the default, credentials remain off tool arguments, and
   strict tool schemas accept the call.
7. Worker ingress is authenticated and PDP-authorized before business work;
   consecutive items cannot see one another's token/history/context.
8. The exact lab-review → medication-order regression denies at authoritative
   score, while approved retrieval/format/order substeps remain aligned.
9. Logs, traces, safe HTTP error details, and final-turn behavior satisfy the
   customer's data-handling requirements; final-turn delivery uses the exact
   RTG HTTPS origin and tested overload/drop behavior.
10. The tested wheel hash and all compatible platform versions are recorded.

## License

Proprietary — © Reva AI, Inc. All rights reserved. Use requires a valid
commercial agreement with Reva. See [LICENSE](LICENSE) or contact
legal@reva.ai.
