Metadata-Version: 2.4
Name: envoys
Version: 0.3.0
Summary: Cryptographic identity for AI agents. Ed25519 keypairs, RFC 9421 HTTP Message Signatures, self-resolving keyids.
Project-URL: Homepage, https://envoys.me
Project-URL: Repository, https://github.com/jschoemaker/Envoys-public
Project-URL: Issues, https://github.com/jschoemaker/Envoys-public/issues
Project-URL: Specification, https://envoys.me/specs/signature/v1
Author-email: jerown <jeroenschoemaker1@gmail.com>
License: Apache-2.0
License-File: LICENSE
Keywords: a2a,agent-identity,ai-agents,cryptography,ed25519,envoys,http-signatures,rfc9421
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
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 :: Cryptography
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: cryptography>=42
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Description-Content-Type: text/markdown

# envoys

[![PyPI](https://img.shields.io/pypi/v/envoys)](https://pypi.org/project/envoys/)
[![Python](https://img.shields.io/pypi/pyversions/envoys)](https://pypi.org/project/envoys/)
[![License](https://img.shields.io/pypi/l/envoys)](https://github.com/jschoemaker/Envoys-public/blob/main/LICENSE)

Cryptographic identity for AI agents. Ed25519 keypairs, RFC 9421 HTTP Message Signatures, self-resolving keyids.

Sign every HTTP request an agent makes, so the receiver can cryptographically verify **which** agent sent it — no shared secret, no central broker. Each agent hosts its own public key at a resolvable URL; verifiers fetch it and check the signature.

Python SDK, requires Python 3.10+. Mirrors [`@envoys/sdk`](https://www.npmjs.com/package/@envoys/sdk) (Node) — same surface, same wire format, byte-compatible against the [`envoys-rfc9421`](https://github.com/jschoemaker/Envoys-public/tree/main/fixtures/envoys-rfc9421) §14 test vectors.

## Install

```bash
pip install envoys
```

## Quick start

### Register an agent

```python
import asyncio
from envoys import Envoys, RegisterOptions

async def main():
    client, result = await Envoys.register(RegisterOptions(
        account_key = "ak_...",        # from envoys.me dashboard
        name        = "scout",
    ))
    print(result.address)       # scout@your-handle.envoys.me
    print(result.private_key)   # PEM PKCS8 — store securely, shown once

asyncio.run(main())
```

### Sign an outgoing HTTP request

`from_env()` reads the agent's credentials from environment variables — populate them from `register`'s result above, or from your envoys.me dashboard.

```python
from envoys import Envoys
import httpx

envoys = Envoys.from_env()   # ENVOYS_AGENT_KEY, ENVOYS_ADDRESS, ENVOYS_PUBLIC_KEY, ENVOYS_PRIVATE_KEY

body    = {"task": "summarize", "url": "https://example.com/doc"}
headers = envoys.sign_request("POST", "/api/task", body)

with httpx.Client() as http:
    r = http.post("https://other-agent.example/api/task",
                  headers={**headers, "Content-Type": "application/json"},
                  json=body)
```

### Verify an incoming request

```python
import asyncio
from envoys import Envoys, VerifyRequestOptions

async def verify(method, path, headers, body):
    result = await Envoys.verify_request(
        method, path, headers, body,
        VerifyRequestOptions(allowlist=["scout@trusted.envoys.me"]),
    )
    if not result.verified:
        return None, result.error
    return result.address, None
```

The verifier enforces component coverage per spec §5.5: signatures must cover
`@method` and `@path`, plus `content-digest` whenever the request has a body —
a signature that leaves the body uncovered is rejected even if
cryptographically valid (digest-downgrade protection, since 0.2.0).

### Optional: bind the signature to the target host (`@authority`)

```python
headers = envoys.sign_request("POST", "/rpc", body, authority="receiver.example.com")
```

Covers RFC 9421 `@authority` so the signature is scoped to one receiving
service — relayed to any other host it fails verification. The verifier
reconstructs the value from `VerifyRequestOptions(authority=...)` (set this
behind a proxy that rewrites Host) or the request's `Host` header. Opt-in:
verifiers older than 0.2.0 reject signatures covering components they don't
reconstruct. Spec v1.6.0 §4.2.

### keyid resolution is SSRF-guarded (since 0.3.0)

The `keyid` is sender-controlled, so resolving it is an untrusted outbound
request. The verifier (`verify_request`, `verify_agent_card`, and the
`resolve_*` helpers) enforces, per spec v1.6.3 §5.4: **https only**,
**rejection of hosts that are or resolve to loopback/private/link-local (incl.
the `169.254.169.254` cloud-metadata range)/CGNAT/non-global addresses**, **no
redirects**, a **16 KB response cap**, and a **5 s timeout**. Override via
`ResolverGuardOptions`:

```python
from envoys import ResolverGuardOptions, VerifyRequestOptions

await Envoys.verify_request(method, path, headers, body, VerifyRequestOptions(
    resolver=ResolverGuardOptions(timeout_s=3.0, max_response_bytes=8192),
))
# Local testing only — re-enables private hosts / http:
await Envoys.resolve_key_from_keyid(
    url, ResolverGuardOptions(allow_private_hosts=True, allow_insecure_http=True),
)
```

DNS rebinding is closed too: resolution is pinned to the validated public
address at connect time (a custom httpx transport resolves once, rejects any
non-public address, and connects to that IP while preserving the original Host
header and TLS SNI), so the address connected to is the address validated. For
hard isolation, also constrain egress at the network layer.

## Surface

Mirrors [`@envoys/sdk`](https://github.com/jschoemaker/Envoys-public/tree/main/packages/sdk) one-to-one with Python-idiomatic naming. Methods shown with `await` are coroutines and must be awaited; the rest are synchronous.

| Operation | Method |
| --- | --- |
| Register a new agent | `await Envoys.register(opts)` |
| Construct from environment | `Envoys.from_env()` |
| Key rotation sync | `await client.sync_keys()` |
| Sign arbitrary payload | `client.sign(payload)` |
| Sign HTTP request (RFC 9421) | `client.sign_request(method, path, body, *, tag=None)` |
| Sign Agent Card (JWS EdDSA) | `client.sign_agent_card(card)` |
| Verify HTTP request | `await Envoys.verify_request(method, path, headers, body, options)` |
| Verify Agent Card | `await Envoys.verify_agent_card(jws)` |
| Verify payload signature | `Envoys.verify(payload, signature, public_key)` |
| Resolve public key by address | `await Envoys.resolve_public_key(address)` |
| Resolve dual-shape keyid | `await Envoys.resolve_key_from_keyid(keyid_url)` |
| Resolve did:web | `await Envoys.resolve_did_web(domain)` |
| Reset pin | `Envoys.reset_pin(address)` |
| Clear caches (testing) | `Envoys.clear_pins()`, `Envoys.clear_replay_cache()`, `Envoys.clear_key_cache()` |

## Spec & conformance

The wire format is published at <https://envoys.me/specs/signature/v1>. Every operation in this SDK byte-matches the §14 reference vectors at [`envoys-rfc9421`](https://github.com/jschoemaker/Envoys-public/tree/main/fixtures/envoys-rfc9421), and cross-implementation interop is verified bidirectionally against [`@envoys/sdk`](https://github.com/jschoemaker/Envoys-public/tree/main/packages/sdk) (Node) — Python signs / Node verifies, and Node signs / Python verifies. The SDK also resolves W3C `did:web` Documents in both `Ed25519VerificationKey2020` and `JsonWebKey2020` shapes.

## License

Apache-2.0
