Metadata-Version: 2.5
Name: archytan-lite
Version: 2.0.0
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://github.com/High-ArchyTech-Solutions/archytan-lite
Project-URL: Repository, https://github.com/High-ArchyTech-Solutions/archytan-lite
Project-URL: Issues, https://github.com/High-ArchyTech-Solutions/archytan-lite/issues
License: MIT
License-File: LICENSE
Keywords: ai-agent,archytan,audit-log,authorization,capability,ed25519,fail-closed,langchain,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](https://github.com/High-ArchyTech-Solutions/archytan-lite)
authorization gate.

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

## 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)
```

`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.

## 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.

## What it does not do

It verifies the signature over the `intent_hash` the gate returned. It does not
recompute that hash from the receipt's fields — doing so would require
byte-for-byte replication of Go's `encoding/json` output, including its
HTML-escaping of `<`, `>` and `&`, and getting that subtly wrong would silently
reject legitimate receipts. Confirming a stored receipt still matches its hash
is `--verify-chain`'s job, server-side, where the canonical encoding lives.
