Metadata-Version: 2.5
Name: archytan-lite
Version: 2.1.1
Summary: Fail-closed client for the Archytan Lite authorization gate. Verifies the gate's Ed25519 signature and that the receipt matches the request, and redeems single-use capability tokens before acting; treats every other outcome as denied.
Project-URL: Homepage, https://high-archy.tech/lite
Project-URL: Documentation, https://github.com/High-ArchyTech-Solutions/archytan-lite-community#readme
Project-URL: Issues, https://github.com/High-ArchyTech-Solutions/archytan-lite-community/issues
Project-URL: Changelog, https://github.com/High-ArchyTech-Solutions/archytan-lite-community/releases
License: MIT
License-File: LICENSE
Keywords: agent-security,ai-agent,ai-agents,archytan,audit-log,authorization,authorization-gate,capability,ed25519,fail-closed,guardrails,langchain,llm,mcp,tamper-evident,zero-trust
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: cryptography>=42.0
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: langchain-core>=0.3; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Provides-Extra: langchain
Requires-Dist: langchain-core>=0.3; extra == 'langchain'
Description-Content-Type: text/markdown

# archytan-lite (Python)

Fail-closed Python client for the Archytan Lite authorization gate —
see [high-archy.tech](https://high-archy.tech).

```sh
pip install archytan-lite
```

## Run the gate

This package is the client; the gate runs as a Docker image on your own
infrastructure. The quickstart starts one and walks through a signed
ALLOW, a BLOCK and a single-use capability in about a minute, checking
every answer as it goes:

```sh
curl -fsSLO https://raw.githubusercontent.com/High-ArchyTech-Solutions/archytan-lite-community/main/quickstart/quickstart.sh
bash quickstart.sh
```

Examples, JSON Schemas, release notes and issues live in
[archytan-lite-community](https://github.com/High-ArchyTech-Solutions/archytan-lite-community).

## The rule this client enforces

Only an HTTP 200 whose body has `decision == "ALLOW"`, **and** a receipt whose
Ed25519 signature verifies against the gate's public key, **and** a receipt that
describes *this* request counts as authorized.

Everything else is denied: a non-200, a timeout, a refused connection, a
malformed body, a signature that doesn't verify, a receipt for some other
action, the gate process being dead.

**Nothing here raises for a denial.** A denial arrives as `allowed=False`, never
as an exception — an exception is something a caller can forget to catch, and a
forgotten `except` around an authorization check fails open.

## Use

```python
from uuid import uuid4
from archytan_lite import authorize, redeem_capability

result = authorize(
    url="http://archytan-lite:8421",
    token=CALLER_TOKEN,
    gate_public_key_hex=GATE_PUBLIC_KEY,   # the hex `keygen` printed
    action="invoice.delete",
    actor={"uid": "user_1"},               # see "Roles" below
    resource={"type": "invoice", "id": "inv_7"},
    idempotency_key=str(uuid4()),
)
if not result.allowed:
    raise PermissionError(result.reason)
```

Pass `traceparent=` (the W3C header value of the current trace) to any of
the four calls and the gate logs its trace and span ids beside the decision,
so the decision shows up next to your trace in your APM. A value that isn't a
valid version-00 traceparent isn't sent, so it can't cause a refusal.

`authorize_async` and `redeem_capability_async` are the async forms. They share
their decision logic with the sync versions rather than reimplementing it — two
copies is how a sync path and an async path end up disagreeing about what
"authorized" means, with only one of them audited.

## Roles

**Omit `actor["role"]` when the gate runs credential-bound roles.** In that mode
your role is a property of your credential, resolved server-side, and a role
asserted in the request that disagrees with it is treated as an escalation
attempt and refused. The receipt comes back carrying the role the gate
resolved.

Send `actor["role"]` only against a gate configured with a single shared token,
which has no other way to learn one. When you do send it, this client checks the
receipt agrees with it.

## Capabilities

When the gate has capabilities enabled, an ALLOW carries a single-use grant
scoped to exactly one action on one resource, expiring in seconds. Holding it is
not permission to act — **spend it immediately before acting**:

```python
spent = redeem_capability(
    url="http://archytan-lite:8421",
    token=CALLER_TOKEN,
    capability_token=result.capability.token,
    action="invoice.delete",
    resource={"type": "invoice", "id": "inv_7"},
)
if not spent.redeemed:
    raise PermissionError(spent.reason)   # expired, out of scope, or already spent

delete_invoice("inv_7")
```

A second redemption of the same capability is refused, so an actuator cannot be
driven to act twice on one authorization.

## AI agent frameworks

```sh
pip install "archytan-lite[langchain]"
```

`ArchytanGuardedTool` wraps a function so an agent cannot reach it without the
gate authorizing the call and the capability being spent first:

```python
from archytan_lite.integrations.langchain import ArchytanGuardedTool

delete_invoice_tool = ArchytanGuardedTool(
    name="delete_invoice",
    description="Permanently delete an invoice by id.",
    action="invoice.delete",
    resource_type="invoice",
    func=really_delete_invoice,          # reached only after ALLOW + redemption
    gate_url="http://archytan-lite:8421",
    caller_token=AGENT_TOKEN,
    gate_public_key_hex=GATE_PUBLIC_KEY,
    actor_uid="agent-invoices",
)
```

It is a LangChain `BaseTool`, which LangGraph and CrewAI both accept, so one
integration covers all three.

A denial returns an explanation to the agent rather than raising, because a
raised exception inside an agent loop is usually swallowed and retried — the
agent should be *told* it was refused, in text it can reason about, so it stops
rather than loops.

**What this cannot do for you:** nothing stops your code from importing the
underlying function and calling it directly. The wrapper makes the guarded path
the easy one and the unguarded path a deliberate act; it is not a sandbox.

## Version 2 receipts

Gates since Archytan Lite 2.4 mint version 2 receipts, which also sign what
each decision was made under: the SHA-256 of the gate's policy and callers
files, the credential that asked (`caller_id`), the resource's tenant, the
gate's mode and whether observe mode turned a refusal into an ALLOW, the
caller's W3C trace id, and the receipt's id, instance, trace id and time.
This client verifies both versions, each under its own signature tag, and
refuses a receipt that claims a version it doesn't know, a v1 receipt that
carries any v2 field, and a v2 receipt with any field changed. Clients older
than 2.1 refuse v2 receipts, so upgrade them before the gate (or run the gate
with `ARCHYTAN_LITE_RECEIPT_VERSION=1` until you have).

Pass `expected_policy_sha256=` (the hex SHA-256 of the policy file you expect
the gate to run) and an ALLOW is accepted only if the receipt says the gate
decided under exactly that policy; a v1 receipt, which records no policy, is
refused. Name the tenant on the request (`resource={"type": ..., "id": ...,
"tenant_id": ...}`) and a v2 receipt for any other tenant is refused too.

## Contract siblings

This client, [`clients/node/index.js`](../node/index.js), and the `authorize()`
helper in `testing/failclosed/failclosed_test.go` are the same contract in three
languages. If you change the decision logic in one, change it in the others.

## How it checks a receipt

The gate signs `intent_hash`, not the fields printed beside it. So before
trusting the signature, this client recomputes `intent_hash` from the receipt's
fields plus the idempotency key it sent, reproducing Go's `encoding/json`
output exactly (HTML escapes, U+2028/U+2029, control characters, raw UTF-8 for
everything else), and pinned to `clients/testdata/intent_vectors.json`, which is
generated from the Go code. Without that step an intermediary could pair a
genuine signature with rewritten fields. `verify_receipt(receipt,
gate_public_key_hex, idempotency_key)` exposes the same check for a stored
receipt; `verify_receipt_signature` alone does not prove the fields were signed.
