Metadata-Version: 2.5
Name: allowly
Version: 0.9.0
Summary: Python SDK for the Allowly API
Project-URL: Repository, https://github.com/Allowly-AI/allowly-sdk-python
License: MIT
License-File: LICENSE
Requires-Python: >=3.10
Requires-Dist: allowly-receipt-format==5.1.0
Requires-Dist: cryptography>=50.0.2
Requires-Dist: httpx>=0.28.1
Provides-Extra: dev
Requires-Dist: cryptography>=50.0.2; extra == 'dev'
Requires-Dist: fastmcp<5,>=4.1.0; extra == 'dev'
Requires-Dist: mcp<3,>=2.3.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=1.4.0; extra == 'dev'
Requires-Dist: pytest>=9.1.1; extra == 'dev'
Requires-Dist: respx>=0.23.1; extra == 'dev'
Provides-Extra: fastmcp
Requires-Dist: fastmcp<5,>=4.1.0; extra == 'fastmcp'
Requires-Dist: mcp<3,>=2.3.0; extra == 'fastmcp'
Description-Content-Type: text/markdown

# Allowly Python SDK

Async Python client for the Allowly runtime API.

## Load hosted agent instructions

```python
from allowly import Allowly

async with Allowly(api_key=ALLOWLY_RUNTIME_KEY) as client:
    instructions = await client.agent_instructions(agent_name="Support agent")
```

Pass the returned text to your agent. It contains the action, resource and field
schema, descriptions, and exact policy reference for that configured name.
In both hosted and copied modes, your agent turns free text into structured
Check JSON; Allowly Check decides permission. Execute only after an `allow`.
Refresh the instructions after policy changes. This method was added in SDK
0.9.0 and requires a runtime supporting `GET /v1/agent-instructions`; earlier SDKs do not
gain it. It uses the runtime API key, rejects invalid text responses, and never
uses an availability fallback.

## Check shared permissions by agent name

After an operator explicitly finishes shared-policy setup, the runtime can
resolve the authorization for a registered, self-reported agent name:

```python
from allowly import Allowly

async with Allowly(api_key=ALLOWLY_RUNTIME_KEY) as client:
    result = await client.check_by_agent(
        expected_policy_ref={"policy_id": "support", "policy_version": 1, "definition_sha256": "a" * 64},
        agent_name="Support agent", actions=["ticket.reply"],
        resource="ticket:one", context={"external": True},
        idempotency_key="support-ticket-one",
    )
    authorization_id = result.authorization_id  # Evidence link, resolved by the server.
    if result.results["ticket.reply"].decision == "allow":
        await send_reply()
```

The name is a routing label, not verified identity. `context.agent_label` is
reserved: the server records `{name, source: "self_reported"}`. Name routing
rejects clients configured with an agent token or token supplier and never
uses availability fallbacks, including configured `fail_open` actions. Stop
on an error or deny; pause confirm/escalate for review. Save the original named
request, including `expected_policy_ref`, context, resource, cost and session. After approval,
call `check_by_agent` again with that same name and unchanged inputs, using a
fresh idempotency key, and execute only a fresh allow. The runtime checks the
current name assignment; the returned authorization ID links evidence and
does not replace the named continuation flow. Existing `check` behavior
and identity-bound authorizations are unchanged. The named method requires SDK
0.9.0 or newer and a runtime supporting `/v1/agent-check`. Copy the exact policy reference from
the generated setup instructions. A policy change requires a refreshed prompt;
a mismatched reference stops the request before policy evaluation.

## Confirmation receipts

Confirmation responses expose `receipt`, a pending resolution envelope or
`None` for an older runtime/replay. The decision is `approved` or
`not_approved`; legacy outcomes are rejected by this fresh rebuild. Poll
`client.receipts.fetch_signed(response.receipt.receipt_id)` when a receipt is
present, then verify it with your configured workspace and trusted keys.
The signature authenticates the recorded client report, not a named human's
identity or approval. Resolution does not dispatch an action. A standalone
Check integration re-checks with its original authorization; native Execute
continues its saved operation as described below.

This OPA rebuild requires the published PyPI package
`allowly-receipt-format==5.1.0` and accepts only receipt wire 5.
Run `uv sync` from this repository; no sibling verifier checkout is required.
Native continuation requires a runtime that supports
`/v1/executions/{operation_id}/continue`; upgrading this SDK does not deploy
that runtime. Earlier SDK packages do not gain these methods.

## Resolution webhook setup and verification

Use a setup/CLI credential to manage the workspace's one existing callback:

```python
import os
from allowly import Allowly, verify_resolution_webhook

async with Allowly(api_key=os.environ["ALLOWLY_SETUP_KEY"]) as setup:
    configured = await setup.resolution_webhook.configure(
        "https://customer.example/allowly-resolution"
    )
    signing_secret = configured.signing_secret  # Store privately for the receiver.
    configuration = await setup.resolution_webhook.get()
    deliveries = await setup.resolution_webhook.deliveries()
    # rotated = await setup.resolution_webhook.rotate()
    # await setup.resolution_webhook.disable()

event = verify_resolution_webhook(
    raw_body, request_headers,
    signing_secret=os.environ["ALLOWLY_RESOLUTION_SIGNING_SECRET"],
    expected_workspace_id=os.environ["ALLOWLY_WORKSPACE_ID"],
)
```

Runtime keys cannot manage callbacks. Only configure/rotate return the signing
secret; configuration reads omit it. Coordinate changes with the existing
receiver: URL changes, re-enabling, and rotation cancel queued older-version
events. The delivery list contains at most 20 summaries, without raw payloads
or secrets. These methods use the existing API contract; no new endpoint is added.

Preserve the exact raw request bytes before JSON parsing and reject duplicate
signature headers at your HTTP boundary. Verification authenticates the fixed
HMAC-SHA256 [Standard Webhooks profile](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md)
with a 300-second attempt-timestamp tolerance and validates
the configured workspace and bounded event fields. It does not prove a human's
identity or verify the referenced receipts. Store event IDs durably before
acknowledging; duplicate deliveries must not become duplicate business jobs.

Select the saved job by its trusted workspace, review kind/ID, and original
source receipt ID. A null or mismatched source cannot select a native Execute
job. Read current prompt status, then wake `continue_http_execution` with the
unchanged original operation and arguments. A verified callback or an approved
status is not dispatch permission; Continue validates current permission before
one claim. Do not run an extra enforcing Check before native continuation.
For standalone Check integrations, perform the existing fresh Check instead.
Rejected, invalid, mismatched, or unavailable review never permits dispatch.

MCP middleware ships inside this SDK: `pip install 'allowly[fastmcp]'`, then `from allowly.mcp import AllowlyMCPMiddleware`. In TypeScript, it lives in the separate `@allowly/mcp` package.

## Author and preview an immutable policy

Action entries may include approved `evidence` requirements keyed by every
declared field: AI `{sources, instruction}`, deterministic
`{source, transform: "email_domain", empty_sources}`, or a boolean
`{all_of: [direct_boolean_field, ...]}`. Requirements are retained in the whole
definition digest. Omitted evidence stays omitted. Rules still use OR.
SDK 0.9.0 uses the published verifier 5.1.0 for these policy and evidence
validation helpers. No sibling checkout or source archive is required.

Policy create/get/list/archive and setup preview/publish require a setup/CLI
credential, not a runtime API key or an agent token. Use the runtime client for
Check and native Execute. The SDK does not change credentials or sign agent
identity for policy setup calls.

```python
from allowly import Allowly, PolicyDefinition

definition: PolicyDefinition = {
    "profile": "allowly.policy.v1",
    "agent_id": "research_agent",
    "actions": [{
        "policy": {
            "profile": "allowly.basic.v1", "action": "web.search",
            "decision": "allow", "target": None,
            "fields": {"risk": "integer"},
            "rules": [{"id": "review-high-risk", "decision": "confirm",
                       "target": "agent_owner",
                       "conditions": [{"field": "risk", "op": "gte", "value": 2}]}],
        },
        "guards": {}, "executable_operations": [],
    }],
    "default_expiry_days": 30,
}

async with Allowly(api_key=ALLOWLY_SETUP_KEY) as setup:
    created = await setup.policies.create(policy_id="research_agent", definition=definition)
    preview = await setup.policies.preview(definition=definition, action="web.search", context={"risk": 2})
    assert preview.provisional
    print(preview.policy_eval.result.decision, preview.policy_eval.input_sha256)
    retained = await setup.policies.get("research_agent")  # Latest active edition.
    page = await setup.policies.list(limit=20)
    # Publish a successor only when ready, naming the exact previous reference:
    # published = await setup.policies.publish(
    #     policy_id="research_agent", definition=updated_definition,
    #     previous_policy_ref=retained.policy_ref, idempotency_key="publish-research-2",
    # )
    # await setup.policies.archive("research_agent")
```

The complete definition and `policy_ref` keep their canonical snake_case JSON
fields, ordered rules, and exact executable grants. Conditions use `eq`, `ne`,
`gt`, `gte`, `lt`, `lte`, `in`, `not_in`, `exists`, `contains_any`,
`contains_none`, or `empty`. A rule matches if any condition matches (OR).
Valid comparisons use fixed priority: deny, escalate, confirm, then allow.
Rules cannot weaken the action decision. Action deny always wins. Otherwise,
a matching rule wins a tie with the action; the first matching rule wins ties
between rules. If no rule matches, the action decision stays unchanged.
List fields support overlap with 1–64 scalar values, or
`empty` with a strict boolean value.
Declared comparison types are strict; integers must be safe JSON integers,
not floats or booleans. List items may mix strings, integers, booleans, and null,
but not nested arrays or objects.
Allow/deny targets are null, confirm targets are `agent_owner`, and escalation
targets are actual `member:<account_user_id>` values. Publishing checks agent
ownership, setup provenance, current membership, and executable catalog bindings
on the server. A guessed member or team label is not a valid review target.

Preview calculates only provisional OPA rules. It does not check grants, live
guards, quotas, budgets, or final execution permission. `policy_eval.result`
can differ from Check's final decision. Parsing it or verifying a receipt
signature is not independent policy replay; replay needs the original retained
artifacts and the pinned official native OPA executable. The SDK has no parallel
Python rule interpreter or unsigned fallback for malformed setup responses.

## Subject authorization pattern

Do not send raw user/customer PII to Allowly receipts unless you intentionally
want it in your audit trail. Create one authorization per subject, store the
returned authorization ID in your own app database, and use that ID for later checks.

```python
import asyncio
import os

from allowly import Allowly


async def main() -> None:
    async with Allowly(
        api_key=os.environ["ALLOWLY_API_KEY"],
        base_url=os.getenv("ALLOWLY_API_URL", "https://api.allowly.ai"),
    ) as allowly:
        # Your app creates a stable internal subject ID.
        subject_id = "subject_abc123"

        # Store this in your app table, for example:
        # allowly_authorizations(subject_id, policy_id, allowly_authorization_id, status)
        authorization = await allowly.authorizations.create(
            user_id=f"subject:{subject_id}",
            policy_id="research_agent",
            policy_version=1,  # The exact retained edition selected during setup.
            metadata={"source": "import"},
        )

        # Before the agent acts, check whether this action is allowed.
        decision = await allowly.check(
            authorization_id=authorization.authorization_id,
            actions=["web.search"],
            resource=f"subject:{subject_id}",
            context={"stage": "research", "risk": 1},
        )

    if decision.results["web.search"].decision != "allow":
        raise RuntimeError("Action is not authorized")


asyncio.run(main())
```

## Allowly agent identity

Create the agent in the dashboard, then run `allowly agent enroll <exact-agent-id>`
from the Allowly CLI. Store the resulting private credential on the trusted
machine that runs the agent. Define its policy, then create a **new authorization**
for that agent. CLI-only setups can still create a live policy before enrollment.
The credential identifies the agent; the authorization and policy still decide
what it may do. The workspace runtime API key is still required.

```python
import os
from allowly import Allowly, NativeAgentCredential

credential = NativeAgentCredential.from_file("/secure/path/agent.json")
allowly = Allowly(
    api_key=os.environ["ALLOWLY_API_KEY"],
    agent_token_supplier=credential.token,
)

decision = await allowly.check(
    authorization_id="auth_...",
    actions=["order.submit"],
)
```

The SDK signs a fresh 60-second token for each request. The private key remains
in your runtime; do not commit or log the credential file. The CLI registers
only its public key with Allowly.

## Existing Auth0 agent identity

For an authorization bound to an Auth0 machine identity, supply the short-lived
access token separately from the Allowly runtime key. A token supplier runs for
each check or execution. Use your existing OAuth client library to fetch and
cache Auth0 tokens; keep the client secret outside this SDK. New self-service
Auth0 setup is unavailable. Contact us to add your own identity provider.

```python
from datetime import datetime, timezone

allowly = Allowly(
    api_key=os.environ["ALLOWLY_API_KEY"],
    agent_token_supplier=get_auth0_agent_token,
)

decision = await allowly.check(
    authorization_id="auth_...",
    actions=["order.submit"],
    client_timestamp=datetime.now(timezone.utc),
)
```

The SDK sends the agent token in the `X-Allowly-Agent-Token` header for checks
and customer-hosted execution calls. Provider credentials stay in your runtime.

## Receipt acknowledgments

After fetching the complete signed receipt, the included verifier can calculate
the canonical receipt hash for an acknowledgment:

```python
from allowly.verify import hash_seal_value, verify_receipt

claims = verify_receipt(signed_receipt, keys, expected_workspace_id=workspace_id)

ack = await allowly.acknowledge_receipt(
    receipt_id=claims["receipt_id"],
    receipt_sha256=hash_seal_value(signed_receipt),
    client_timestamp=datetime.now(timezone.utc),
    idempotency_key=f"ack:{claims['receipt_id']}",
)

# Retrieve the same acknowledgment later by its returned ID.
ack = await allowly.get_receipt_acknowledgment(
    claims["receipt_id"],
    ack.acknowledgment_id,
)
```

Client timestamps are customer-reported and must include a timezone. They do
not replace the timestamp issued by Allowly in a receipt.

## Execute from customer code

Enable an app in **Settings → Executables**, then bind its exact operation to
the policy action. Enabling an app alone does not grant permission. The required
receipt verifier is included with this SDK.

```python
from allowly import Allowly

async with Allowly(api_key=ALLOWLY_RUNTIME_KEY, agent_token=AGENT_TOKEN) as allowly:
    result = await allowly.execute_http(
        "https://api.stripe.com/v1/refunds",
        operation_id="refund-order-123",  # stable across retries
        authorization_id="auth_...",
        enabled_executable_id="exe_...",
        catalog_operation_id="stripe.refunds.create",
        action="billing.refund",
        method="POST",
        headers={"authorization": f"Bearer {STRIPE_KEY}",
                 "content-type": "application/x-www-form-urlencoded"},
        body="payment_intent=pi_example&amount=2500",
        evidence_mode="receipt",
    )
    # result.response stays local; Allowly receives commitments and outcome metadata.
```

Use the operation ID from the catalog returned by your deployment; the initial
catalog contains documentation-checked candidates, not a live compatibility
guarantee. Policy inputs such as `policy_input={"context": {"amount": 2500}}`
are customer-reported. A receipt does not prove
that those inputs match the meaning of the provider body.

The helper calls remote `/v1/execute`, validates the approval, and claims dispatch
once before sending locally. It fails closed on unavailable checks and stops on
deny. Confirmation and escalation return `status="waiting_for_review"` with a
typed `review` containing its kind, opaque ID, source receipt ID, and expiry.
No provider request is sent while review is pending. Low-level customer methods are `prepare_execution`,
`continue_execution`, `claim_execution_dispatch`,
`report_execution_outcome`, and `get_execution`.

Read the review without running another Check or creating a receipt:

```python
if result.execution.review.kind == "confirm":
    status = await allowly.confirmations.get_status(result.execution.review.id)
else:
    status = await allowly.escalations.get_status(result.execution.review.id)
```

These typed responses return the recorded choice, source/resolution receipt IDs,
and current grant lifecycle. Use the opaque `cnf_` monitor ID for confirmation,
never its bearer nonce. `status="approved"` is only a wake-up signal: continue
the saved operation so Allowly validates current permission. Null or unknown
evidence is not an allow. `await allowly.readiness()` reads `/readyz`; readiness
is not permission, and network/protocol errors raise rather than returning an allow.

Allowly receives the origin, path, query string and policy inputs. Header values
and body bytes are committed by hash. Keep provider credentials in local headers,
not in the URL, query string or policy context.

Keep the original URL, method, headers, body, and policy inputs in your own
durable job store. A verified approval webhook can wake that job; polling the
approval resource is another option. A webhook is not permission to send.
Continue with the **same arguments and operation ID**:

```python
result = await allowly.continue_http_execution(original_url, **original_arguments)
```

`original_arguments` must include the original `operation_id`, authorization,
executable, action, headers, body, policy inputs, evidence mode, and journal
directory. Restore credentials from your local credential store. The SDK checks
the saved request hash and sends only the original request commitments and review
binding to `/v1/executions/{operation_id}/continue`. The runtime validates the
review and current permission before the SDK claims dispatch. Still-pending
review remains waiting; rejected, expired, or revoked permission never sends.
Do not call a separate enforcing Check before continuation: continuation itself
performs the fresh evaluation, including any one-use escalation grant.

The private journal defaults to `.allowly/executions`; put it on persistent
storage and use a separate directory per workspace. It retains the original
prepare timestamp, request hash, review binding, and outcome, not raw provider
headers, request body, or confirmation bearer nonce. The initial response can
carry that nonce for your confirmation flow; do not log it. Version-1 journals and incomplete initial prepare
attempts cannot be reopened through continuation. A repeated Execute raises
`ExecutionRecoveryRequired` with its directory. Reconcile with `get_execution`
or call `flush_execution_outcome(operation_dir)` to retry a saved report. Neither
repeats the provider action. An interrupted or timed-out send may already have
acted; never generate a replacement operation ID automatically. There is no
exactly-once guarantee for arbitrary providers. If `result.outcome_pending` is
true, its API response still describes approval; `result.response` contains the
locally observed response and the journal retains the report for upload.
The Python helper also raises `ExecutionRecoveryRequired` for a duplicate
completed continuation instead of returning a cached local response. Use
`get_execution` to read the outcome; do not turn that error into another send.

Signing stays asynchronous. To assemble the Allowly side of the evidence later:

```python
from allowly import complete_execution_evidence

package = await complete_execution_evidence(
    allowly, result.operation_dir,
    public_keys=trusted_workspace_keys, expected_workspace_id="ws_...",
)
```

This verifies the decision receipt signature and its exact approval hash. The outcome remains customer-reported, not independent proof of execution or business completion.

## FastMCP identity mapping

FastMCP middleware maps policy inputs explicitly. Raw tool arguments are
available to the callback, but the middleware does not copy them into Allowly
context. Provide `trusted_user_id` from your server's authenticated session or
token.

```python
from allowly.mcp import AllowlyMCPMiddleware, MCPCheckInput

middleware = AllowlyMCPMiddleware(
    api_key=os.environ["ALLOWLY_API_KEY"],
    user_id_fn=trusted_user_id,
    authorization_id_fn=lambda user_id: authorization_id_for(user_id),
    agent_token_fn=lambda request: auth0_agent_token_for(request.fastmcp_context),
    check_input_fn=lambda request: MCPCheckInput(
        action="email.send",
        resource=f"gmail:thread:{request.arguments['thread_id']}",
        context={"recipient_domain": request.arguments["recipient_domain"]},
        client_timestamp=datetime.now(timezone.utc),
        idempotency_key=request.arguments["operation_id"],
    ),
)
mcp.add_middleware(middleware)
```

The three resolver callbacks may be synchronous or asynchronous. When an
`agent_token_fn` is configured, an error or an empty result blocks the tool
before the check. Map only the fields the policy needs; ordinary tool arguments
do not automatically satisfy policy context.

Local development against the documented Caddy endpoint requires the edge
token that Cloudflare injects for public traffic. Pass it explicitly:

```python
Allowly(
    api_key=os.environ["ALLOWLY_API_KEY"],
    base_url="http://localhost:8443",
    dangerously_allow_insecure_base_url=True,
    edge_token=os.environ["ALLOWLY_EDGE_TOKEN"],
)
```

The token is only sent when provided; never set it for the public API.

## Send JSON through a private SEAL webhook

Copy the private URL from the dashboard's **SEAL** page. The URL is the only
credential this client sends; it does not use an ordinary API key.

```python
import asyncio
import os

from allowly import SealWebhookClient


async def seal_event(raw_json: str, event_id: str):
    async with SealWebhookClient(os.environ["ALLOWLY_SEAL_WEBHOOK_URL"]) as webhook:
        delivery = await webhook.send(
            raw_json,
            idempotency_key=event_id,
            type="invoice",
            reference="INV-1042",
            statement="Approved for payment",
        )
        while delivery.status in {"received", "signing"}:
            await asyncio.sleep(1)
            delivery = await webhook.get_delivery(delivery.attempt_id)
        if delivery.status != "sealed":
            raise RuntimeError(delivery.error_code or "SEAL delivery failed")
        return delivery.receipt, await webhook.get_keys()
```

The webhook processes your JSON to create a fingerprint; Allowly stores the
fingerprint and signed receipt. Keep the original record in your workflow.
Receipt details are sent in the three explicit `Allowly-Seal-*` headers. Their
values must use printable ASCII, may contain interior spaces, and must not have
leading or trailing whitespace. The client rejects invalid values instead of
changing them. Direct API metadata still supports its existing Unicode values.
When a signed receipt is present, the client returns its signed metadata and
rejects a conflicting top-level delivery projection.
Treat the full URL like a password and keep it out of logs, tickets, and source
control. Regenerating or disabling it stops the old URL. Delivery associations
and status remain available for 7 days; preserve signed receipts and keys under
your own retention policy. With no `idempotency_key`, retrying after a lost
response can create another seal.

## Seal a JSON record with local hashing

`seal` hashes strict raw JSON in your process, sends only its digest to Allowly,
and waits for the full signed receipt. Generate and persist `request_id` in
your workflow so a retry recovers the same seal:

```python
import uuid

request_id = str(uuid.uuid4())
sealed = await allowly.seal(
    raw_json,
    request_id=request_id,
    metadata={"source": "invoice-workflow"},
)
save_beside_record(sealed.receipt)
```

Use `seal_value(parsed_json, ...)` only when the original JSON text is no
longer available. A parsed value cannot reveal duplicate object names or the
original number spelling, so `seal` is the safer input boundary.

To verify later, preserve the authenticated `workspace_id` response and a key
document fetched from Allowly through an authenticated or previously trusted
source. Keep the signature and record checks separate:

```python
from allowly.verify import load_keys_from_json, verify_seal_json

result = verify_seal_json(
    raw_json,
    sealed.receipt,
    load_keys_from_json(keys_doc),
    expected_workspace_id=sealed.workspace_id,
    trusted_key_fingerprints=configured_key_fingerprints,
)
assert result.signature_verified
assert result.record_matches
```

Authorization creation requires a logical `policy_id` and exact positive
`policy_version`. Optional `actions` is a names-only subset of that edition;
omit it to select all its actions. Inline policy rules, action objects,
`agent_id`, constraints, and decision-override maps are not accepted.

Unavailable checks fail closed unless an action is explicitly mapped to
`"fail_open"` with `fallback_by_action`. Unmapped actions always fail closed.
Identity-enabled checks always fail closed, including token supplier failures
and `identity_verification_unavailable` responses.

For actions that need third-party approval, define the escalation rule on the
agent policy, create the authorization from its exact `policy_id` and
`policy_version`, and then resolve
returned escalation results with
`await allowly.escalations.approve(escalation_id, resolved_by="manager:123")`
or `reject(...)`, then re-check before running the action.

Read a confirmation or escalation without resolving it:

```python
# confirmation_id comes from a confirm check result. It is not confirm_nonce.
confirmation = await allowly.confirmations.get(confirmation_id)
escalation = await allowly.escalations.get(escalation_id)
print(confirmation.status, confirmation.authority_status)
print(escalation.status, escalation.authority_status)
```

The released `get` names remain aliases of `get_status`; both return the same
typed status object. `ConfirmationStatusResponse` and `EscalationStatusResponse`
are aliases of the existing `ConfirmationStatus` and `EscalationStatus` types.
Each read retries once if the server disconnects without a response. A second
failure or malformed response raises an error; keep the workflow paused. These
GET retries never replay a Check or other write. Repeat reads in your
application's own bounded polling loop if needed. The prompt `status` is `pending`, `approved`,
`rejected`, `expired`, or `unknown`; `unknown` means a legacy record does not
show the choice. An approved choice stays approved after its grant expires,
is revoked, or (for escalations) is consumed. `authority_status="available"`
is a lifecycle snapshot, not permission. For standalone Check integrations,
make a fresh `allowly.check(...)` with the original authorization and execute
only an `allow`. For native Execute, continue the saved original operation;
do not insert an extra enforcing Check. These reads never execute actions,
consume approval, or create receipts. Nullable receipt IDs refer to existing
records. Older check responses may omit `confirmation_id`.

If you need lookup by email later, import `from_email` from
`allowly.identifiers` and store `from_email(email, pepper=APP_PII_PEPPER)`.
The helper trims and lowercases only, prefixes the result with `email_hmac:v1`,
and never sends the raw email or pepper to Allowly. Keep the pepper stable and
backed up; changing it changes derived user IDs. Keep raw names, emails,
documents, and profile URLs out of Allowly receipts unless those fields are
intentionally part of your audit record.

Do not add raw HTTP fallbacks in application code for APIs the SDK is missing.
Patch this SDK first, then use the typed client from the app. That keeps the
integration examples honest and makes SDK gaps visible early.

## Offline receipt verification

Use the included verifier to hash SEAL records and verify signed receipts.
This SDK pins the published `allowly-receipt-format==5.1.0`, which verifies
receipt wire format 5 (the package major equals the wire format). `alg` and
the key ID are in the signed JWS protected header. The receipt has only
`protected`, `payload`, and `signature` fields, all base64url strings. Store and
share that original envelope. `verify_receipt` returns verified claims for
decisions and request binding. `decode_receipt` only reads claims for display;
it does not prove their signature or grant permission.

Wire format 5 preserves daily `receipt.checkpoint` commitments. For an
untrusted receipt bundle, pass caller-trusted key fingerprints to
`verify_receipt`; a fingerprint copied from that same bundle is not a trust
anchor.

Key-document fetching requires HTTPS by default. For the documented local
Caddy endpoint only, pass `dangerously_allow_insecure_base_url=True` and its
`edge_token` to `fetch_keys_doc`, matching the client options above.
