Getting started
Install
pip install fg-amp # core (no web dependencies)
pip install "fg-amp[http]" # + HTTP transport (FastAPI, aiohttp, uvicorn)
Requires Python ≥ 3.11. Core dependencies are fg-agent-id, pydantic (v2), and
cryptography; the http extra adds everything the relay, HTTP transport, and wake
receiver need. The package is fully typed (py.typed ships in the wheel).
Naming note: the installable dist fg-amp, the import package fg_amp, the amp-relay
console script, and the wire id amp/0.1 are stable public identifiers — they were
deliberately not renamed when the repo was rebranded.
Hello world
The shortest working conversation — two connected in-process nodes from
fg_amp.testing, no relay, no network:
import asyncio
from fg_amp.testing import amp_pair
async def main():
async def respond(session): # runs as its own task — receiving here is safe
message = await session.receive()
await session.send_text(f"pong ({message.payload.content})")
a, b = await amp_pair(on_session=respond) # two connected in-process nodes
session = await a.initiate(b.card, purpose="hello")
await session.send_text("ping")
print((await session.receive(timeout=1)).payload.content) # pong (ping)
asyncio.run(main())
amp_pair(on_session=...)returns two fresh nodes wired over one in-memory transport, both open-policy — the right default for a test double.on_sessionis the responder pattern: the callback fires with the establishedSessionafter an inbound handshake completes. Callbacks are spawned as their own tasks, so awaitingsession.receive()inside one is safe — it cannot deadlock the dispatch loop that delivers the message.initiate→send_text→receiveis the full handshake and an end-to-end encrypted round trip.
One call to the network
AmpNode.create collapses construct → attach → connect. Give it a relay URL
(http(s):// for HTTP polling, ws(s):// for WebSocket push with HTTP fallback), an
explicit Transport, or nothing for a private in-memory transport:
from fg_amp import AgentIdentity, AmpNode, ContactPolicy
identity = AgentIdentity.load_or_create("agent-keys.fgid") # persisted keypair
async with await AmpNode.create(
identity, relay="wss://relay.example", policy=ContactPolicy.open()
) as node:
session = await node.initiate(peer_card, purpose="hello over the relay")
await session.send_text("ping")
Two things to know:
createdefaults to a closed policy. Unlike the bare constructor (historically open), acreated node can call out but accepts no inbound initiations until you opt in with an explicitpolicy=— reachability on the network is a decision, not a default.- Nodes are async context managers.
async withcallsaclose()on exit: close frames for live sessions, in-flight handshakes failed, transport detached.
Under the one-liners: manual wiring
The same session, built from the parts amp_pair assembles for you:
import asyncio
from fg_amp import AgentIdentity, AmpNode, ContactPolicy, InMemoryTransport
async def main():
inbound = []
async def on_session(session):
inbound.append(session)
alice = AmpNode(identity=AgentIdentity.generate("alice"))
bob = AmpNode(identity=AgentIdentity.generate("bob"),
policy=ContactPolicy.open(), on_session=on_session)
transport = InMemoryTransport()
alice.attach(transport)
bob.attach(transport)
session = await alice.initiate(bob.card, purpose="price negotiation")
await session.send_text("Offering 100 units at $4.20 — interested?")
message = await inbound[0].receive(timeout=1)
print(message.sender, "→", message.payload.content)
await session.close()
asyncio.run(main())
Line by line
AgentIdentity.generate("alice")mints a fresh Ed25519 keypair. The resulting address (amp:key:<base58>) is the public key — nothing to register anywhere. UseAgentIdentity.load_or_create(path)when the identity should survive restarts.AmpNode(identity=...)is the composition root: it owns the identity, runs the handshake, enforces policy, and manages sessions. The bare constructor defaults to an open policy (useAmpNode.createfor the closed-by-default variant).policy=ContactPolicy.open()says "anyone with a valid signature, card, and delegation chain may knock." Policy is evaluated in code, on every initiation — it is the consent gate.on_session=on_sessionis how Bob learns a peer got through: the callback fires with the establishedSessionafter the handshake completes, as its own task.InMemoryTransport()is a same-process mesh — perfect for tests and examples. Both nodesattachto it; envelopes queue for addresses that aren't bound yet and flush on bind. Swap inRelayTransportorHttpTransportfor the network without changing any other line.alice.initiate(bob.card, purpose=...)performs the full handshake: a sealedhandshake.initiateknock, Bob's policy check, a sealedhandshake.accept, and session-key derivation. It returns an established, end-to-end-encryptedSession.send_text/receivemove payloads through the double ratchet — every message gets a fresh AEAD key.message.senderis the verified peer address, andmessage.payload.contentis the decrypted text. Areceive(timeout=...)that expires raises aTimeoutErrornaming the session id and the timeout.session.close()sends an authenticated close and burns the session's key material.
Running a relay
For agents on different machines, run a relay — untrusted infrastructure that hosts encrypted mailboxes and a signed-card directory:
pip install "fg-amp[http]" && amp-relay --port 8404
Add --peer URL --sync-interval N to federate with another relay (cards and
revocations sync; mailboxes deliberately do not). The relay never sees plaintext —
only signed ciphertext envelopes and routing metadata.
Agents reach it in one call — AmpNode.create(identity, relay="http://relay-host:8404")
— and discover peers via resolve_card(address) — see
Examples.