ritta 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ name: ci
2
+ on: [push, pull_request]
3
+ jobs:
4
+ test:
5
+ runs-on: ubuntu-latest
6
+ steps:
7
+ - uses: actions/checkout@v4
8
+ - uses: astral-sh/setup-uv@v3
9
+ - run: uv sync
10
+ - run: uv run ruff check .
11
+ - run: uv run mypy --strict src
12
+ - run: uv run pytest -q
@@ -0,0 +1,44 @@
1
+ name: publish
2
+
3
+ # Publishes to PyPI when a GitHub release is published from a v* tag.
4
+ # Uses PyPI trusted publishing (OIDC): no API tokens are stored in secrets.
5
+ on:
6
+ release:
7
+ types: [published]
8
+
9
+ jobs:
10
+ build:
11
+ if: startsWith(github.event.release.tag_name, 'v')
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: astral-sh/setup-uv@v3
16
+ - name: Check tag matches project version
17
+ run: |
18
+ version=$(grep -m1 '^version = ' pyproject.toml | cut -d '"' -f2)
19
+ test "v${version}" = "${{ github.event.release.tag_name }}" \
20
+ || { echo "Tag ${{ github.event.release.tag_name }} != v${version}"; exit 1; }
21
+ - run: uv sync
22
+ - run: uv run ruff check .
23
+ - run: uv run mypy --strict src
24
+ - run: uv run pytest -q
25
+ - run: uv build
26
+ - uses: actions/upload-artifact@v4
27
+ with:
28
+ name: dist
29
+ path: dist/
30
+
31
+ publish:
32
+ needs: build
33
+ runs-on: ubuntu-latest
34
+ environment:
35
+ name: pypi
36
+ url: https://pypi.org/p/ritta
37
+ permissions:
38
+ id-token: write
39
+ steps:
40
+ - uses: actions/download-artifact@v4
41
+ with:
42
+ name: dist
43
+ path: dist/
44
+ - uses: pypa/gh-action-pypi-publish@release/v1
ritta-0.2.0/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .mypy_cache/
8
+ .ruff_cache/
9
+ .pytest_cache/
10
+ .hypothesis/
11
+ .env
12
+ .env.*
13
+ *.pem
14
+ *.key
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## 0.2.0
6
+
7
+ - Renamed to Ritta: the PyPI distribution and the import package are now `ritta` (was `agentmandates`).
8
+ - Claim prefix changed from `agentmandates:` to `ritta:` (e.g. `ritta:limits.total`). Tokens with the old prefix are rejected as `malformed`; see the compatibility note in the spec.
9
+ - No functional changes.
10
+
11
+ ## 0.1.0
12
+
13
+ - `Mandate`, `Limits` and `Scope` models (pydantic v2) with every claim required by the spec; integer amounts only.
14
+ - Ed25519 key generation and PEM import/export (`generate_keypair`, `*_to_pem`, `load_*_pem`).
15
+ - Mandates signed as SD-JWT (`typ: vc+sd-jwt`, `alg: EdDSA`), no selective disclosure yet.
16
+ - Pure `verify()` returning `VerificationResult(ok, reason)`.
17
+ - Spec: defined wire format, check order, empty `categories` means "any", verify does not enforce `hitl_threshold`.
18
+ - READMEs in Spanish, English and Portuguese. Package ships `py.typed`.
19
+ - Project scaffold.
ritta-0.2.0/CLAUDE.md ADDED
@@ -0,0 +1,61 @@
1
+ # CLAUDE.md — mandates
2
+
3
+ Este archivo es la memoria de trabajo de Claude Code en este repo. Leelo entero antes de tocar código.
4
+
5
+ ## Qué es el proyecto (en una oración)
6
+
7
+ > Nombre: **Ritta** (ritta.io, docs en ritta.dev). Repo: https://github.com/ritta-io/mandates. Paquete en PyPI: `ritta`. Publicar en PyPI solo desde un tag `v*` (release de GitHub → `.github/workflows/publish.yml`), nunca a mano ni desde una rama.
8
+
9
+ El producto le pone límites a la inteligencia artificial cuando gasta plata: el dueño define hasta cuánto puede pagar un agente y en qué, cada pago se verifica contra esa regla antes de ejecutarse, y todo queda registrado de forma auditable. La plata nunca pasa por nosotros: pasa por Mercado Pago, Pix o una wallet del cliente.
10
+
11
+ ## Qué es ESTE repo
12
+
13
+ `mandates` es la **librería open source (Apache 2.0)** del formato de mandato y su verificación, más el SDK de cliente en Python. Es la capa de adquisición: lo que un desarrollador instala con `pip install ritta` para que su agente pueda pedir permiso de gasto de forma estándar.
14
+
15
+ Lo que vive acá:
16
+ - `ritta.mandates`: modelo de mandato, firma, verificación, serialización. Compatible con **AP2 v0.2** (Agent Payments Protocol, hoy en la FIDO Alliance): el mandato es una credencial verificable **SD-JWT**, con estados `open` (presupuesto + restricciones) y `closed` (monto atado a un checkout concreto).
17
+ - `ritta.client`: cliente HTTP y herramientas MCP para hablar con un el gateway (`create_mandate`, `quote`, `request_payment`, `get_receipt`).
18
+ - `docs/`: spec del mandato en español, inglés y portugués.
19
+
20
+ Lo que NO vive acá (vive en `gateway`, repo privado): el motor de políticas, los rieles de pago (Mercado Pago, x402, Pix), el ledger, el audit log, el panel, la facturación.
21
+
22
+ ## Stack y convenciones
23
+
24
+ - Python 3.12. Gestor: `uv`. Empaquetado con `pyproject.toml` (hatchling). Nombre del paquete en PyPI: `ritta`.
25
+ - Tipado estricto: `mypy --strict` pasa siempre. Modelos con `pydantic` v2.
26
+ - Firma y verificación: `cryptography` (Ed25519 por defecto) y SD-JWT según el borrador IETF `draft-ietf-oauth-selective-disclosure-jwt`. No inventar criptografía: usar primitivas estándar.
27
+ - Tests con `pytest` + `hypothesis` para propiedades (un mandato nunca se verifica con firma inválida, vencido, o con monto mayor al restante).
28
+ - Lint y formato: `ruff`. Commits en inglés, estilo Conventional Commits (`feat:`, `fix:`, `docs:`). Código, nombres y comentarios en inglés; documentación de usuario en español, inglés y portugués.
29
+ - Sin dependencias pesadas. Nada de frameworks web en este repo.
30
+
31
+ ## Reglas no negociables
32
+
33
+ 1. **El modelo de lenguaje nunca decide monto, moneda ni destinatario.** Esos datos salen del mandato firmado y del catálogo del comercio. El agente solo elige un `product_id` o una intención. Cualquier API que acepte un monto libre proveniente del agente está mal diseñada y no se mergea.
34
+ 2. **Nada de secretos en el código ni en los tests.** Claves de prueba se generan en el test. Nunca commitear `.env`, tokens de Mercado Pago, claves privadas.
35
+ 3. **Un mandato tiene siempre:** emisor (quién autoriza), sujeto (qué agente), límites (monto total, monto por operación, moneda), alcance (comercios o categorías permitidas), vencimiento, nonce, y firma. Si falta uno, el constructor falla.
36
+ 4. **Verificación es pura y determinista:** misma entrada, misma salida, sin red. La verificación de un mandato no llama a ningún servicio externo.
37
+ 5. **Compatibilidad AP2 primero.** Si hay que elegir entre un diseño propio y uno que mapea limpio a AP2/Verifiable Credentials, se elige AP2. Lo que no cubre AP2 va en extensiones con prefijo `ritta:`.
38
+ 6. **Idempotencia:** toda operación que pueda repetirse lleva `idempotency_key`.
39
+ 7. **Montos en enteros** (centavos/unidad mínima) con moneda ISO 4217 o identificador de token. Nunca `float`.
40
+
41
+ ## Cómo trabajamos con Claude Code
42
+
43
+ - Antes de implementar algo, leé `docs/spec-mandate.md` y los tests existentes. Si la spec no cubre el caso, primero se actualiza la spec, después el código.
44
+ - Trabajá en ramas `feat/...` o `fix/...`. Un PR por cambio. Los PR llevan descripción en inglés y, si cambia la spec, una línea en `CHANGELOG.md`.
45
+ - Definición de terminado: `uv run pytest` verde, `uv run mypy --strict src` sin errores, `uv run ruff check .` limpio, docs actualizadas en los tres idiomas si cambió la API pública.
46
+ - Cuando no estés seguro de una decisión de diseño que afecte la compatibilidad con AP2, Mercado Pago o x402: no asumas, dejá un comentario `# DECISION NEEDED:` y pará.
47
+ - No agregues funcionalidad que no esté en la etapa actual del roadmap (ver `docs/roadmap.md`). Menos es más.
48
+
49
+ ## Comandos
50
+
51
+ ```bash
52
+ uv sync # instalar dependencias
53
+ uv run pytest -q # tests
54
+ uv run mypy --strict src # tipos
55
+ uv run ruff check . && uv run ruff format . # lint y formato
56
+ uv build # construir el paquete
57
+ ```
58
+
59
+ ## Estado actual
60
+
61
+ Etapa 1 del roadmap. Objetivo de la etapa: esquema de mandato compatible con AP2 v0.2 (SD-JWT), firma y verificación en Python, README en tres idiomas, tests de propiedades. Ver `docs/roadmap.md`.
ritta-0.2.0/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2026 Ramiro Ritta
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
ritta-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.5
2
+ Name: ritta
3
+ Version: 0.2.0
4
+ Summary: Signed spend mandates for AI agents: create, sign and verify what an agent may pay, on whose behalf, and within which limits.
5
+ Project-URL: Homepage, https://ritta.io
6
+ Project-URL: Documentation, https://ritta.dev
7
+ Project-URL: Repository, https://github.com/ritta-io/mandates
8
+ Author: Ramiro Ritta
9
+ License-Expression: Apache-2.0
10
+ License-File: LICENSE
11
+ Keywords: agentic-commerce,ai-agents,ap2,mandates,mcp,mercadopago,payments,x402
12
+ Classifier: Development Status :: 2 - Pre-Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Office/Business :: Financial
17
+ Classifier: Topic :: Security :: Cryptography
18
+ Requires-Python: >=3.12
19
+ Requires-Dist: cryptography>=42
20
+ Requires-Dist: httpx>=0.27
21
+ Requires-Dist: pydantic>=2.7
22
+ Provides-Extra: mcp
23
+ Requires-Dist: mcp>=1.2; extra == 'mcp'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # Ritta · mandates
27
+
28
+ **Español** · [English](docs/README_en.md) · [Português](docs/README_pt.md)
29
+
30
+ [Ritta](https://ritta.io) le pone límites a la inteligencia artificial cuando gasta tu plata: vos decís hasta cuánto puede pagar un agente y en qué, cada pago se verifica contra esa regla antes de ejecutarse, y todo queda registrado. La plata nunca pasa por Ritta.
31
+
32
+ Este repo es la librería open source (Apache 2.0) del **mandato**: el permiso firmado que dice "este agente puede gastar hasta X, en Y, en nombre de Z". Es compatible con [AP2](https://fidoalliance.org/) (Agent Payments Protocol) y se usa con el gateway para pagar con Mercado Pago, Pix y USDC.
33
+
34
+ ```bash
35
+ pip install ritta
36
+ ```
37
+
38
+ ```python
39
+ from ritta import Limits, Mandate, Scope, generate_keypair, sign, verify
40
+
41
+ private_key, public_key = generate_keypair()
42
+
43
+ mandate = Mandate(
44
+ issuer="did:web:farmacia-central.com.ar",
45
+ subject="agent:compras-01",
46
+ limits=Limits(total=15_000_000, per_operation=5_000_000, currency="ARS"), # centavos
47
+ scope=Scope(merchants=["MP-123456"], categories=["pharmacy"]),
48
+ hitl_threshold=1_000_000, # desde $10.000 se pide aprobación humana (lo aplica el gateway)
49
+ state="open",
50
+ expires_in_days=30,
51
+ )
52
+ token = sign(mandate, private_key) # SD-JWT firmado con Ed25519
53
+
54
+ result = verify(
55
+ token,
56
+ public_key,
57
+ amount=120_000, # en centavos; sale del catálogo del comercio, nunca del modelo
58
+ merchant="MP-123456",
59
+ category="pharmacy",
60
+ remaining=15_000_000, # saldo sin gastar, lo aporta el ledger del gateway
61
+ )
62
+ assert result.ok
63
+ ```
64
+
65
+ Si el pago no está autorizado, `result.ok` es `False` y `result.reason` dice por qué: `invalid_signature`, `expired`, `amount_over_per_operation`, `amount_over_remaining`, `merchant_not_allowed`, `category_not_allowed` o `malformed`.
66
+
67
+ `verify` es pura: no usa red ni guarda estado. Misma entrada, misma salida (pasá `now=` para fijar el reloj).
68
+
69
+ Las claves se exportan e importan en PEM con `ritta.mandates.private_key_to_pem`, `public_key_to_pem`, `load_private_key_pem` y `load_public_key_pem`. Nunca guardes una clave privada en el repositorio.
70
+
71
+ Documentación: [ritta.dev](https://ritta.dev). La especificación del formato está en [`docs/spec-mandate.md`](docs/spec-mandate.md).
72
+
73
+ ## Estado
74
+
75
+ Pre-alpha (0.2.0). Etapa 1 del roadmap: ver [`docs/roadmap.md`](docs/roadmap.md).
76
+
77
+ ## Publicación (mantenedores)
78
+
79
+ Las versiones se publican en PyPI como `ritta` solo desde un release de GitHub con tag `v*` (por ejemplo `v0.2.0`, que tiene que coincidir con `version` en `pyproject.toml`). Lo hace `.github/workflows/publish.yml` con *trusted publishing*: no hay tokens de PyPI en los secretos del repo.
80
+
81
+ Configuración, una sola vez:
82
+
83
+ 1. En pypi.org, con la cuenta dueña del proyecto: *Your account → Publishing → Add a new pending publisher* (o, si el proyecto ya existe, *Manage → Publishing*).
84
+ 2. Completar: PyPI project name `ritta`, owner `ritta-io`, repository `mandates`, workflow name `publish.yml`, environment name `pypi`.
85
+ 3. En GitHub, *Settings → Environments* del repo `ritta-io/mandates`: crear el environment `pypi` (conviene agregar *required reviewers* para aprobar cada publicación).
86
+ 4. Publicar: actualizar `version` y `CHANGELOG.md`, mergear a `main`, y crear el release con tag `vX.Y.Z` desde la pestaña *Releases*.
87
+
88
+ ## Licencia
89
+
90
+ Apache 2.0.
ritta-0.2.0/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # Ritta · mandates
2
+
3
+ **Español** · [English](docs/README_en.md) · [Português](docs/README_pt.md)
4
+
5
+ [Ritta](https://ritta.io) le pone límites a la inteligencia artificial cuando gasta tu plata: vos decís hasta cuánto puede pagar un agente y en qué, cada pago se verifica contra esa regla antes de ejecutarse, y todo queda registrado. La plata nunca pasa por Ritta.
6
+
7
+ Este repo es la librería open source (Apache 2.0) del **mandato**: el permiso firmado que dice "este agente puede gastar hasta X, en Y, en nombre de Z". Es compatible con [AP2](https://fidoalliance.org/) (Agent Payments Protocol) y se usa con el gateway para pagar con Mercado Pago, Pix y USDC.
8
+
9
+ ```bash
10
+ pip install ritta
11
+ ```
12
+
13
+ ```python
14
+ from ritta import Limits, Mandate, Scope, generate_keypair, sign, verify
15
+
16
+ private_key, public_key = generate_keypair()
17
+
18
+ mandate = Mandate(
19
+ issuer="did:web:farmacia-central.com.ar",
20
+ subject="agent:compras-01",
21
+ limits=Limits(total=15_000_000, per_operation=5_000_000, currency="ARS"), # centavos
22
+ scope=Scope(merchants=["MP-123456"], categories=["pharmacy"]),
23
+ hitl_threshold=1_000_000, # desde $10.000 se pide aprobación humana (lo aplica el gateway)
24
+ state="open",
25
+ expires_in_days=30,
26
+ )
27
+ token = sign(mandate, private_key) # SD-JWT firmado con Ed25519
28
+
29
+ result = verify(
30
+ token,
31
+ public_key,
32
+ amount=120_000, # en centavos; sale del catálogo del comercio, nunca del modelo
33
+ merchant="MP-123456",
34
+ category="pharmacy",
35
+ remaining=15_000_000, # saldo sin gastar, lo aporta el ledger del gateway
36
+ )
37
+ assert result.ok
38
+ ```
39
+
40
+ Si el pago no está autorizado, `result.ok` es `False` y `result.reason` dice por qué: `invalid_signature`, `expired`, `amount_over_per_operation`, `amount_over_remaining`, `merchant_not_allowed`, `category_not_allowed` o `malformed`.
41
+
42
+ `verify` es pura: no usa red ni guarda estado. Misma entrada, misma salida (pasá `now=` para fijar el reloj).
43
+
44
+ Las claves se exportan e importan en PEM con `ritta.mandates.private_key_to_pem`, `public_key_to_pem`, `load_private_key_pem` y `load_public_key_pem`. Nunca guardes una clave privada en el repositorio.
45
+
46
+ Documentación: [ritta.dev](https://ritta.dev). La especificación del formato está en [`docs/spec-mandate.md`](docs/spec-mandate.md).
47
+
48
+ ## Estado
49
+
50
+ Pre-alpha (0.2.0). Etapa 1 del roadmap: ver [`docs/roadmap.md`](docs/roadmap.md).
51
+
52
+ ## Publicación (mantenedores)
53
+
54
+ Las versiones se publican en PyPI como `ritta` solo desde un release de GitHub con tag `v*` (por ejemplo `v0.2.0`, que tiene que coincidir con `version` en `pyproject.toml`). Lo hace `.github/workflows/publish.yml` con *trusted publishing*: no hay tokens de PyPI en los secretos del repo.
55
+
56
+ Configuración, una sola vez:
57
+
58
+ 1. En pypi.org, con la cuenta dueña del proyecto: *Your account → Publishing → Add a new pending publisher* (o, si el proyecto ya existe, *Manage → Publishing*).
59
+ 2. Completar: PyPI project name `ritta`, owner `ritta-io`, repository `mandates`, workflow name `publish.yml`, environment name `pypi`.
60
+ 3. En GitHub, *Settings → Environments* del repo `ritta-io/mandates`: crear el environment `pypi` (conviene agregar *required reviewers* para aprobar cada publicación).
61
+ 4. Publicar: actualizar `version` y `CHANGELOG.md`, mergear a `main`, y crear el release con tag `vX.Y.Z` desde la pestaña *Releases*.
62
+
63
+ ## Licencia
64
+
65
+ Apache 2.0.
@@ -0,0 +1,54 @@
1
+ # Ritta · mandates
2
+
3
+ [Español](../README.md) · **English** · [Português](README_pt.md)
4
+
5
+ [Ritta](https://ritta.io) puts limits on AI when it spends your money: you decide how much an agent may pay and on what, every payment is checked against that rule before it runs, and everything is recorded. The money never goes through Ritta.
6
+
7
+ This repo is the open source (Apache 2.0) library for the **mandate**: the signed permission that says "this agent may spend up to X, on Y, on behalf of Z". It is compatible with [AP2](https://fidoalliance.org/) (Agent Payments Protocol) and is used with the gateway to pay with Mercado Pago, Pix and USDC.
8
+
9
+ ```bash
10
+ pip install ritta
11
+ ```
12
+
13
+ ```python
14
+ from ritta import Limits, Mandate, Scope, generate_keypair, sign, verify
15
+
16
+ private_key, public_key = generate_keypair()
17
+
18
+ mandate = Mandate(
19
+ issuer="did:web:farmacia-central.com.ar",
20
+ subject="agent:compras-01",
21
+ limits=Limits(total=15_000_000, per_operation=5_000_000, currency="ARS"), # cents
22
+ scope=Scope(merchants=["MP-123456"], categories=["pharmacy"]),
23
+ hitl_threshold=1_000_000, # from $10,000 a human must approve (enforced by the gateway)
24
+ state="open",
25
+ expires_in_days=30,
26
+ )
27
+ token = sign(mandate, private_key) # SD-JWT signed with Ed25519
28
+
29
+ result = verify(
30
+ token,
31
+ public_key,
32
+ amount=120_000, # in cents; comes from the merchant catalog, never from the model
33
+ merchant="MP-123456",
34
+ category="pharmacy",
35
+ remaining=15_000_000, # unspent balance, supplied by the gateway ledger
36
+ )
37
+ assert result.ok
38
+ ```
39
+
40
+ If the payment is not authorized, `result.ok` is `False` and `result.reason` says why: `invalid_signature`, `expired`, `amount_over_per_operation`, `amount_over_remaining`, `merchant_not_allowed`, `category_not_allowed` or `malformed`.
41
+
42
+ `verify` is pure: no network, no state. Same input, same output (pass `now=` to pin the clock).
43
+
44
+ Keys are exported and imported as PEM with `ritta.mandates.private_key_to_pem`, `public_key_to_pem`, `load_private_key_pem` and `load_public_key_pem`. Never store a private key in the repository.
45
+
46
+ Documentation: [ritta.dev](https://ritta.dev). The format specification is in [`spec-mandate.md`](spec-mandate.md) (Spanish).
47
+
48
+ ## Status
49
+
50
+ Pre-alpha (0.2.0). Stage 1 of the roadmap: see [`roadmap.md`](roadmap.md).
51
+
52
+ ## License
53
+
54
+ Apache 2.0.
@@ -0,0 +1,54 @@
1
+ # Ritta · mandates
2
+
3
+ [Español](../README.md) · [English](README_en.md) · **Português**
4
+
5
+ A [Ritta](https://ritta.io) coloca limites na inteligência artificial quando ela gasta o seu dinheiro: você decide quanto um agente pode pagar e em quê, cada pagamento é verificado contra essa regra antes de ser executado, e tudo fica registrado. O dinheiro nunca passa pela Ritta.
6
+
7
+ Este repositório é a biblioteca open source (Apache 2.0) do **mandato**: a permissão assinada que diz "este agente pode gastar até X, em Y, em nome de Z". É compatível com o [AP2](https://fidoalliance.org/) (Agent Payments Protocol) e é usada com o gateway para pagar com Mercado Pago, Pix e USDC.
8
+
9
+ ```bash
10
+ pip install ritta
11
+ ```
12
+
13
+ ```python
14
+ from ritta import Limits, Mandate, Scope, generate_keypair, sign, verify
15
+
16
+ private_key, public_key = generate_keypair()
17
+
18
+ mandate = Mandate(
19
+ issuer="did:web:farmacia-central.com.ar",
20
+ subject="agent:compras-01",
21
+ limits=Limits(total=15_000_000, per_operation=5_000_000, currency="ARS"), # centavos
22
+ scope=Scope(merchants=["MP-123456"], categories=["pharmacy"]),
23
+ hitl_threshold=1_000_000, # a partir de $10.000 exige aprovação humana (aplicada pelo gateway)
24
+ state="open",
25
+ expires_in_days=30,
26
+ )
27
+ token = sign(mandate, private_key) # SD-JWT assinado com Ed25519
28
+
29
+ result = verify(
30
+ token,
31
+ public_key,
32
+ amount=120_000, # em centavos; vem do catálogo do comerciante, nunca do modelo
33
+ merchant="MP-123456",
34
+ category="pharmacy",
35
+ remaining=15_000_000, # saldo não gasto, fornecido pelo ledger do gateway
36
+ )
37
+ assert result.ok
38
+ ```
39
+
40
+ Se o pagamento não for autorizado, `result.ok` é `False` e `result.reason` diz o motivo: `invalid_signature`, `expired`, `amount_over_per_operation`, `amount_over_remaining`, `merchant_not_allowed`, `category_not_allowed` ou `malformed`.
41
+
42
+ `verify` é pura: não usa rede nem guarda estado. Mesma entrada, mesma saída (passe `now=` para fixar o relógio).
43
+
44
+ As chaves são exportadas e importadas em PEM com `ritta.mandates.private_key_to_pem`, `public_key_to_pem`, `load_private_key_pem` e `load_public_key_pem`. Nunca guarde uma chave privada no repositório.
45
+
46
+ Documentação: [ritta.dev](https://ritta.dev). A especificação do formato está em [`spec-mandate.md`](spec-mandate.md) (em espanhol).
47
+
48
+ ## Status
49
+
50
+ Pré-alfa (0.2.0). Etapa 1 do roadmap: veja [`roadmap.md`](roadmap.md).
51
+
52
+ ## Licença
53
+
54
+ Apache 2.0.
@@ -0,0 +1,17 @@
1
+ # Roadmap público
2
+
3
+ | Etapa | Entregable |
4
+ |---|---|
5
+ | 1 | Esquema de mandato AP2 v0.2 (SD-JWT), firma/verificación Ed25519, README en es/en/pt, tests de propiedades |
6
+ | 2 | (gateway) Modelo de datos Postgres: tenants, agents, mandates, ledger doble entrada, audit log hash-chain |
7
+ | 3 | (gateway) Mercado Pago sandbox con `mercadopago-commerce-agents-checkout`: Order, webhook, conciliación |
8
+ | 4 | Servidor MCP (OAuth 2.1) con `create_mandate`, `quote`, `request_payment`, `get_receipt`. Demo en video |
9
+ | 5 | (gateway) Policy engine: límites agente/sesión/comercio/día + kill switch. Suite de 20 ataques de prompt injection |
10
+ | 6 | Hito 1: primer pago real en producción de Mercado Pago (ARS 100) |
11
+ | 7 | x402 en Base Sepolia → mainnet: primer servicio cobrando por llamada |
12
+ | 8 | Segundo y tercer servicio de nicho. Publicación en Bazaar y Agent.market |
13
+ | 9 | Registros de MCP. Opinión legal PSP. Términos y privacidad |
14
+ | 10 | Dataset de fallas v0: 10 categorías, 50 casos anonimizados |
15
+ | 11 | Plan Builder con cobro. Panel mínimo |
16
+ | 12 | Hito 3: 3+ usuarios externos, 3 servicios cobrando, primer cliente pago |
17
+ | 13 | Retro pública: seguir, pivotar o parar |
@@ -0,0 +1,52 @@
1
+ # Especificación del mandato Ritta (borrador v0, implementado en 0.2.0)
2
+
3
+ Un **mandato** es una credencial verificable (SD-JWT) que autoriza a un agente a gastar dentro de límites. Mapea a AP2 v0.2; lo que AP2 no cubre va en claims con prefijo `ritta:`.
4
+
5
+ ## Claims obligatorios
6
+
7
+ Los nombres son literales y planos: `ritta:limits.total` es un único claim, no un objeto anidado.
8
+
9
+ > **Compatibilidad.** La versión 0.1.0 usaba el prefijo `agentmandates:`. Desde 0.2.0 el prefijo es `ritta:` y un verificador 0.2.0 **no acepta** mandatos con el prefijo anterior: los rechaza como `malformed` porque les faltan los claims obligatorios. No se ofrece migración porque 0.1.0 no tuvo usuarios externos.
10
+
11
+ | Claim | Tipo | Significado |
12
+ |---|---|---|
13
+ | `iss` | DID o URL | Quién autoriza (persona o empresa). Hoy se acepta `did:<método>:...` o `https://...` |
14
+ | `sub` | string | Identificador del agente autorizado |
15
+ | `ritta:limits.total` | int | Monto total autorizado, en unidad mínima (> 0) |
16
+ | `ritta:limits.per_operation` | int | Tope por operación (> 0 y ≤ `total`) |
17
+ | `ritta:limits.currency` | string | ISO 4217 (`ARS`, `BRL`) o token (`USDC`). Sintaxis: `^[A-Z][A-Z0-9]{2,9}$` |
18
+ | `ritta:scope.merchants` | lista | IDs de comercio permitidos (vacío = cualquiera, desaconsejado) |
19
+ | `ritta:scope.categories` | lista | Categorías permitidas (vacío = cualquiera, desaconsejado) |
20
+ | `ritta:hitl_threshold` | int | Monto a partir del cual se exige aprobación humana (≥ 0) |
21
+ | `exp` | timestamp | Vencimiento (NumericDate, segundos UTC) |
22
+ | `jti` | string | Nonce único (anti-replay) |
23
+ | `ritta:state` | `open` / `closed` | Abierto (presupuesto) o cerrado (atado a un checkout) |
24
+
25
+ Los montos son enteros JSON. Un número con decimales (aunque sea `100.0`) o un string hace que el mandato sea `malformed`.
26
+
27
+ ## Formato (SD-JWT)
28
+
29
+ El mandato firmado es un SD-JWT en forma compacta: `<JWT firmado por el emisor>~`.
30
+
31
+ - Header: `{"alg": "EdDSA", "typ": "vc+sd-jwt"}`. Firma Ed25519.
32
+ - Payload: `"_sd_alg": "sha-256"` más los claims de la tabla.
33
+ - El `~` final es el separador de SD-JWT con cero disclosures y sin Key Binding JWT.
34
+
35
+ En 0.1.0 no hay divulgación selectiva: todos los claims son visibles. El formato ya es SD-JWT válido, así que agregar disclosures después no rompe el sobre. Mientras tanto, un verificador 0.1.0 rechaza como `malformed` cualquier token con disclosures, Key Binding JWT, digests `_sd`, header con `crit`, miembros JSON duplicados o base64url no canónico.
36
+
37
+ ## Verificación
38
+
39
+ `verify(token, public_key, *, amount, merchant, category, remaining, now=None)` devuelve `ok` solo si: la firma es válida, `now < exp`, `amount <= per_operation`, `amount <= remaining`, `merchant` y `category` están dentro del scope. Es pura: no consulta red. El saldo `remaining` lo aporta el gateway (ledger); la librería recibe el valor.
40
+
41
+ Si falla, devuelve la primera razón en este orden: `malformed` / `invalid_signature`, `expired`, `amount_over_per_operation`, `amount_over_remaining`, `merchant_not_allowed`, `category_not_allowed`.
42
+
43
+ - `amount` y `remaining` son enteros en la unidad mínima de la moneda del mandato. Argumentos inválidos (`amount <= 0`, `remaining < 0`, no enteros, `now` sin zona horaria) son errores de programación y lanzan excepción.
44
+ - `hitl_threshold` no lo aplica `verify`: pedir aprobación humana es una decisión de política del gateway.
45
+
46
+ ## Pendientes (DECISION NEEDED)
47
+
48
+ - Formato exacto de `iss` para pymes argentinas sin DID (¿`did:web` del gateway + CUIT?).
49
+ - Mapeo de `ritta:scope.categories` a MCC de tarjetas.
50
+ - `typ`: el borrador actual de SD-JWT VC usa `dc+sd-jwt` (antes `vc+sd-jwt`) y exige el claim `vct`. Confirmar qué esperan los verificadores AP2 v0.2.
51
+ - Mandato `closed`: qué claim lo ata al checkout (CartMandate de AP2). Hoy se verifica igual que uno `open`.
52
+ - Vinculación del agente (holder binding, claim `cnf` + Key Binding JWT): no implementada.
@@ -0,0 +1,60 @@
1
+ [project]
2
+ name = "ritta"
3
+ version = "0.2.0"
4
+ description = "Signed spend mandates for AI agents: create, sign and verify what an agent may pay, on whose behalf, and within which limits."
5
+ readme = "README.md"
6
+ license = "Apache-2.0"
7
+ requires-python = ">=3.12"
8
+ authors = [{ name = "Ramiro Ritta" }]
9
+ keywords = ["ai-agents", "payments", "mandates", "ap2", "mcp", "mercadopago", "x402", "agentic-commerce"]
10
+ classifiers = [
11
+ "Development Status :: 2 - Pre-Alpha",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: Apache Software License",
14
+ "Programming Language :: Python :: 3.12",
15
+ "Topic :: Office/Business :: Financial",
16
+ "Topic :: Security :: Cryptography",
17
+ ]
18
+ dependencies = [
19
+ "pydantic>=2.7",
20
+ "cryptography>=42",
21
+ "httpx>=0.27",
22
+ ]
23
+
24
+ [project.optional-dependencies]
25
+ mcp = ["mcp>=1.2"]
26
+
27
+ [project.urls]
28
+ Homepage = "https://ritta.io"
29
+ Documentation = "https://ritta.dev"
30
+ Repository = "https://github.com/ritta-io/mandates"
31
+
32
+ [build-system]
33
+ requires = ["hatchling"]
34
+ build-backend = "hatchling.build"
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/ritta"]
38
+
39
+ [dependency-groups]
40
+ dev = [
41
+ "pytest>=8",
42
+ "hypothesis>=6",
43
+ "mypy>=1.10",
44
+ "ruff>=0.5",
45
+ ]
46
+
47
+ [tool.ruff]
48
+ line-length = 100
49
+ target-version = "py312"
50
+
51
+ [tool.ruff.lint]
52
+ select = ["E", "F", "I", "B", "UP", "S"]
53
+ ignore = ["S101"]
54
+
55
+ [tool.mypy]
56
+ strict = true
57
+ python_version = "3.12"
58
+
59
+ [tool.pytest.ini_options]
60
+ testpaths = ["tests"]
@@ -0,0 +1,24 @@
1
+ """Ritta: signed spend mandates for AI agents."""
2
+
3
+ from ritta.mandates import (
4
+ Limits,
5
+ Mandate,
6
+ Scope,
7
+ VerificationResult,
8
+ generate_keypair,
9
+ sign,
10
+ verify,
11
+ )
12
+
13
+ __version__ = "0.2.0"
14
+
15
+ __all__ = [
16
+ "Limits",
17
+ "Mandate",
18
+ "Scope",
19
+ "VerificationResult",
20
+ "__version__",
21
+ "generate_keypair",
22
+ "sign",
23
+ "verify",
24
+ ]
File without changes
@@ -0,0 +1,27 @@
1
+ """Mandate model, signing (SD-JWT, Ed25519) and pure verification."""
2
+
3
+ from ritta.mandates.keys import (
4
+ generate_keypair,
5
+ load_private_key_pem,
6
+ load_public_key_pem,
7
+ private_key_to_pem,
8
+ public_key_to_pem,
9
+ )
10
+ from ritta.mandates.model import Limits, Mandate, Scope
11
+ from ritta.mandates.sdjwt import sign
12
+ from ritta.mandates.verify import RejectionReason, VerificationResult, verify
13
+
14
+ __all__ = [
15
+ "Limits",
16
+ "Mandate",
17
+ "RejectionReason",
18
+ "Scope",
19
+ "VerificationResult",
20
+ "generate_keypair",
21
+ "load_private_key_pem",
22
+ "load_public_key_pem",
23
+ "private_key_to_pem",
24
+ "public_key_to_pem",
25
+ "sign",
26
+ "verify",
27
+ ]