Async Python library for decentralized, hubless federation between peer directory services.
Project description
federlet
Federlet is an async Python library for decentralized, hubless federation between peer directory services. It provides signed manifests, signed HTTP requests, Ed25519 verification, replay protection, key-continuity checks, local admission policy, SSRF-safe manifest fetching, health probing, revocations, and peer discovery helpers.
Use federlet when independent services need zero-trust, service-to-service federation without a central registry or control-plane hub. Your application keeps control of HTTP routing, persistence, trust policy, key storage, semantic search, observability, and deployment topology.
At a glance
| Question | Answer |
|---|---|
| What is it? | A framework-neutral protocol library for peer directory federation. |
| Trust model | Signed manifests, Ed25519 signed requests, local admission policy, and key-continuity checks. |
| Runtime | Async Python, httpx, Pydantic v2, structural protocols for host-owned storage. |
| Deployment shape | No central hub, no bundled server, no required cache backend. |
| Host responsibilities | HTTP routing, persistence, private-key storage, trust material, semantic search, logging, metrics, and retries. |
Features
federlet implements the protocol core from ADR-005:
- signed node manifests
- signed HTTP request envelopes for peer-to-peer calls
- Ed25519/JWK helpers and RFC 8785 canonical JSON signing
- freshness, clock-skew, target, method, path, body-hash, and replay checks
- local manifest admission policy and key-continuity checks
- SSRF protection for manifest URLs and admitted endpoints
- membership, revocation, manifest refresh, and discovery state helpers
- protocol, health, revocation, capability-summary, and membership client calls
- structural protocols for host-owned nonce caches, rate limiters, and stores
- typed Pydantic models and
py.typedpackaging
federlet does not run your service. It is a framework-neutral protocol library you wire into your HTTP adapter, worker, or service runtime.
Install
Use uv by default:
uv add federlet
For development in this repository:
uv sync
uv run ruff check
uv run ruff format --check
uv run mypy src
uv run pytest
If your service wants the optional recommended replay-cache backend:
uv add "federlet[cashews]"
With pip:
pip install federlet
pip install "federlet[cashews]"
When to use federlet
Use federlet when independent directory nodes need to discover each other and make signed, auditable requests without a central hub. Typical examples:
- two organizations already trust each other and want signed peer requests
- a new organization wants to join an existing federation through an introduction flow
- a service wants protocol semantics without adopting a bundled server framework
Do not use federlet as a complete federation server. It is the protocol library you wire into your HTTP adapter, worker, or service runtime.
federlet deliberately does not implement semantic directory search, record fetch, query fan-out, coverage calculation, principal mapping, namespace authorization, or registry policy. Those belong to the host application.
Core concepts
| Concern | federlet provides | Your application provides |
|---|---|---|
| Manifests | Pydantic wire models, fetch-time verification, signing, freshness checks | key lifecycle, publication URL, revision policy |
| Signed requests | envelope creation and verification | request routing and response handling |
| Replay protection | NonceCache protocol and nonce-claim logic |
the cache object passed at verification time |
| Rate limiting | RateLimiter protocol and in-memory TokenBucketRateLimiter |
distributed per-peer limiter state |
| Admission | policy checks and verifier callback port | trust material and evidence validation rules |
| Federation calls | async httpx helpers for manifests, introductions, and members |
peer selection, retries policy, logging, metrics |
| Server | no server | your HTTP stack, routing, middleware, and deployment runtime |
Quick start
This example runs without a server. It creates two node manifests, admits one peer under local policy, signs a request from node A to node B, and verifies it with replay protection.
import asyncio
from datetime import datetime, timedelta, timezone
from federlet import (
AdmissionPolicy,
Manifest,
Membership,
PublicKey,
admit_manifest,
build_signed_request,
find_jwk,
generate_key,
public_jwk,
sign_manifest,
verify_signed_request,
)
class MemoryNonceCache:
def __init__(self) -> None:
self._seen: set[str] = set()
async def set(
self,
key: str,
value: object,
expire: float | None = None,
exist: bool | None = None,
) -> bool:
if exist is False and key in self._seen:
return False
self._seen.add(key)
return True
def make_manifest(
*,
node_id: str,
org_id: str,
endpoint: str,
key,
key_id: str,
) -> Manifest:
now = datetime.now(timezone.utc)
manifest = Manifest(
node_id=node_id,
org_id=org_id,
federations=["supplier-network-prod"],
endpoint=endpoint,
protocol_versions=["agent-directory-federation/1"],
revision=1,
public_keys=[PublicKey(key_id=key_id, public_jwk=public_jwk(key))],
membership=Membership(
introduce_url=f"{endpoint}/members/introduce",
members_url=f"{endpoint}/members",
),
issued_at=now,
expires_at=now + timedelta(days=7),
)
return sign_manifest(manifest, key, key_id)
async def main() -> None:
key_a = generate_key()
key_b = generate_key()
manifest_a = make_manifest(
node_id="dir:org-a:prod",
org_id="org-a",
endpoint="https://dir-a.example/federation/v1",
key=key_a,
key_id="org-a-k1",
)
manifest_b = make_manifest(
node_id="dir:org-b:prod",
org_id="org-b",
endpoint="https://dir-b.example/federation/v1",
key=key_b,
key_id="org-b-k1",
)
decision = await admit_manifest(
manifest_b,
AdmissionPolicy(
federation_id="supplier-network-prod",
protocol_versions={"agent-directory-federation/1"},
),
)
assert decision.accepted, decision.reason
body = b'{"operation":"example"}'
envelope = build_signed_request(
key_a,
"org-a-k1",
federation_id="supplier-network-prod",
source_node_id=manifest_a.node_id,
target_node_id=manifest_b.node_id,
method="POST",
path="/federation/v1/example",
body=body,
source_manifest_revision=manifest_a.revision,
)
assert envelope.signature is not None
jwk = find_jwk(manifest_a.public_keys, envelope.signature.key_id)
assert jwk is not None
ok, reason = await verify_signed_request(
envelope,
jwk,
self_node_id=manifest_b.node_id,
method="POST",
path="/federation/v1/example",
body=body,
cache=MemoryNonceCache(),
)
assert ok, reason
print("verified")
asyncio.run(main())
Publish your signed manifest at a stable HTTPS URL controlled by your service. Your HTTP adapter can then use the same verification function for inbound peer requests.
Host adapter sketch
Inbound federation endpoints should parse the detached signature envelope, choose the sender's current public key from its trusted manifest, then verify the request against the actual method, path, target node, and body bytes.
Replay protection is the one place federlet needs cache semantics. Pass any object
that implements NonceCache.set(key, value, expire=..., exist=False). A
cashews.Cache works directly; in production, back it with Redis or Valkey.
Omitting cache disables replay protection and should be limited to tests or
special-purpose verification.
from cashews import Cache
from federlet import SIGNATURE_HEADER, SignedRequest, find_jwk, verify_signed_request
nonce_cache = Cache()
nonce_cache.setup("redis://redis.internal:6379/0")
class UnauthorizedPeerRequest(ValueError):
pass
async def verify_peer_request(
*,
signature_header: str | None,
method: str,
path: str,
body: bytes,
peer_manifest,
self_node_id: str,
) -> None:
if not signature_header:
raise UnauthorizedPeerRequest("missing_signature")
envelope = SignedRequest.model_validate_json(signature_header)
if envelope.signature is None:
raise UnauthorizedPeerRequest("unsigned")
jwk = find_jwk(peer_manifest.public_keys, envelope.signature.key_id)
if jwk is None:
raise UnauthorizedPeerRequest("unknown_key")
ok, reason = await verify_signed_request(
envelope,
jwk,
self_node_id=self_node_id,
method=method,
path=path,
body=body,
cache=nonce_cache,
)
if not ok:
raise UnauthorizedPeerRequest(reason)
Your HTTP adapter decides how to map UnauthorizedPeerRequest to a response
status and how to obtain the header value, for example from SIGNATURE_HEADER.
The nonce key is scoped by federation, source node, target node, and nonce. It is claimed only after the signature, target, method, path, timestamp, and body hash are valid. Failed unauthenticated requests do not consume nonces.
For per-peer request throttling, hosts can inject anything that implements the
RateLimiter protocol. TokenBucketRateLimiter is an in-memory reference
implementation that reads Manifest.limits.max_query_rps_per_peer; production
deployments should keep the bucket state in Redis, Valkey, or an equivalent
shared store.
For audit logging, audit_record(...) builds a flat ADR-shaped dict with an
ISO-Z timestamp. Feed that dict to your JSON logger, JSONL sink, or SIEM
adapter; federlet does not own logging transport.
Admission
Admission is local policy. federlet validates the manifest signature, freshness, federation id, protocol version, signed-HTTP support, HTTPS endpoint, and optional endpoint domain limits. Stronger evidence, such as SPIFFE identities, partner credentials, or charter keys, belongs in your callback.
from federlet import AdmissionPolicy, admit_manifest, domain_evidence_verifier
policy = AdmissionPolicy(
federation_id="supplier-network-prod",
protocol_versions={"agent-directory-federation/1"},
allowed_endpoint_domains={"example"},
evidence_verifier=domain_evidence_verifier,
)
decision = await admit_manifest(peer_manifest, policy)
if not decision.accepted:
raise ValueError(f"peer rejected: {decision.reason}")
Protocol client
FederationClient verifies fetched manifests, signs outbound requests, and
verifies signed introduction and membership responses.
from federlet import FederationClient
async with FederationClient(
node_id="dir:org-a:prod",
federation_id="supplier-network-prod",
key=key,
key_id=key_id,
manifest_revision=signed_manifest.revision,
) as client:
peer_manifest = await client.fetch_manifest(org_b_manifest_url)
members = await client.get_members(peer_manifest)
Your application decides what to do with accepted peer manifests and member references. federlet only signs and verifies the protocol exchange.
Module Map
| Module | Purpose |
|---|---|
federlet.models |
Pydantic wire models for manifests, introductions, membership, signatures, and signed request envelopes. |
federlet.crypto |
Ed25519/JWK conversion, base64url helpers, and RFC 8785 canonical JSON bytes. |
federlet.signing |
Manifest signing/checking and signed request construction/verification. |
federlet.audit |
Pure audit record builder for host logging sinks. |
federlet.admission |
Local manifest admission checks and host-supplied evidence verifier protocol. |
federlet.membership |
In-memory membership state helpers; persistence remains host-owned. |
federlet.refresh |
One-shot manifest refresh and key-continuity decision helper. |
federlet.discovery |
Bounded peer discovery from signed membership hints. |
federlet.health |
Protocol and health probe classification helpers. |
federlet.net |
SSRF guard for manifest and endpoint URLs. |
federlet.client |
Async httpx helpers for manifest fetch, introduction, members, revocations, capability summaries, protocol, and health calls. |
federlet.protocols |
Structural protocols such as NonceCache, RateLimiter, and MembershipStore for Mongo/Postgres-backed hosts. |
Usage scenarios
Scenario: existing peers exchange membership
- Org A and Org B exchange signed manifest URLs through an existing trust path.
- Each service fetches and verifies the other's manifest.
- Each service admits the peer with its local
AdmissionPolicy. - Org A calls
get_members(org_b_manifest). - Org B verifies the signed request and signs the membership response.
- Org A verifies the response signature and treats returned members as discovery hints.
This is the simplest steady-state deployment. No central directory is required.
Scenario: a new peer joins
- Org C starts with one or more seed manifest URLs for the federation.
- Org C fetches and verifies those manifests with
fetch_manifest. - Org C sends a signed
IntroduceRequestto seed peers. - Each seed peer admits or rejects Org C independently.
- Org C calls
get_membersto learn additional manifest URLs. - The host may include Org C in its own query or routing layer once local membership state marks it active.
The integration test in tests/test_federation.py exercises this flow with
three local nodes.
Scenario: a peer is slow or unhealthy
- The host probes or calls eligible peers on its own schedule.
- The host records timeouts and transport failures.
MembershipTablecan model cooldown for repeatedly failing peers.- A later host-observed success moves the peer back to active.
This keeps local peer selection useful during partial outages without making federlet own a background scheduler.
Production notes
- Store private keys in your platform key manager or secret store, not in code.
- Rotate keys by publishing overlapping
public_keysin the manifest and advancingrevision. - Publish manifests over HTTPS and set
expires_at; admission requires expiry by default. - Keep
allow_private=Falseoutside local tests so manifest fetching and admission reject private, loopback, link-local, and reserved endpoints. - Treat
domain_evidence_verifieras a minimal sample for domain-shaped claims. Use your own verifier for real organizational trust. - Add application metrics around admission decisions, verification failures, and peer cooldown state.
Development
uv sync
uv run ruff check
uv run ruff format --check
uv run mypy src
uv run pytest
The tests include:
- manifest signing, freshness, and tamper checks
- signed request replay protection
- admission policy failures
- SSRF guard behavior
- introduction, membership exchange, and rejection scenarios
- revocation, capability-summary, health, refresh, and discovery flows
API surface
Primary imports are re-exported from federlet:
from federlet import (
AdmissionDecision,
AdmissionPolicy,
CapabilitySummary,
DiscoveryOutcome,
DiscoveryRefreshReport,
EvidenceVerifier,
FederationClient,
HealthResponse,
IntroduceRequest,
IntroduceResponse,
JWK,
KeyContinuityDecision,
KeyContinuityPolicy,
Manifest,
ManifestLimits,
ManifestRefreshDecision,
ManifestVerificationError,
MemberRecord,
MemberRef,
Membership,
MembersResponse,
MembershipTable,
MissingCapabilitySummaryEndpointError,
MissingRevocationsEndpointError,
NonceCache,
PeerHealthProbeResult,
PeerState,
ProtocolResponse,
PublicKey,
RateLimiter,
RevocationNotice,
RevocationsResponse,
ResponseSignatureError,
SIGNATURE_HEADER,
SSRFError,
Signature,
SignedRequest,
TokenBucketRateLimiter,
admit_manifest,
apply_revocation_notice,
b64u_decode,
b64u_encode,
build_signed_request,
canonical_bytes,
check_key_continuity,
check_manifest,
disclose_members,
domain_evidence_verifier,
find_jwk,
generate_key,
probe_peer_health,
public_jwk,
public_key_from_jwk,
refresh_discovered_members,
refresh_peer_manifest,
sha256_hex,
sign_dict,
sign_manifest,
sign_model,
verify_dict,
verify_manifest,
verify_model,
verify_response_signature,
verify_revocation_notice,
verify_signed_request,
)
The lower-level signing helpers are public so downstream tests and host
adapters can construct signed fixtures without importing federlet.signing
directly. Production request verification should still go through
verify_signed_request, because it performs target, method, path, body-hash,
timestamp, signature, and nonce checks in one place.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file federlet-0.1.0.tar.gz.
File metadata
- Download URL: federlet-0.1.0.tar.gz
- Upload date:
- Size: 90.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d28ac45e0faea109b032743833d2cba0055787fa301907707191448abbf8fa76
|
|
| MD5 |
21c58da76adbe037e7a9661c3aa1dd2c
|
|
| BLAKE2b-256 |
9dbb26018b117998ebef205caaf8c2cb1736b54257f47f8d5fac5ad7cf75580b
|
File details
Details for the file federlet-0.1.0-py3-none-any.whl.
File metadata
- Download URL: federlet-0.1.0-py3-none-any.whl
- Upload date:
- Size: 30.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
117fa5ce98f4c8f07f783e8fa7d695741d3683cf8826932d48e1e01242025fa2
|
|
| MD5 |
c7806ca55b5c4c67fe1f5df2540d0001
|
|
| BLAKE2b-256 |
2c3e0d8f09ae871c731dfe46d45ce57aba1b3f33483a9212c5477a4499b2c7c2
|