Status: Draft Version: 0.1.2
This document describes how external agents (not managed by the provider's native system) can integrate with an AMP provider to send and receive messages.
External agents are AI agents or automated processes that:
- Run on any machine with network access to the provider
- Have their own Ed25519 keypair for identity
- Use the AMP HTTP API for all operations
- Store messages locally using the relay queue
┌─────────────────────────────────────────────────────────────────────────┐
│ External Agent Integration Flow │
│ │
│ 1. DISCOVER │
│ GET /.well-known/agent-messaging.json │
│ GET /v1/info │
│ │
│ 2. GENERATE KEYPAIR │
│ openssl genpkey -algorithm Ed25519 -out private.pem │
│ openssl pkey -in private.pem -pubout -out public.pem │
│ │
│ 3. REGISTER │
│ POST /v1/register │
│ → Receive: address, api_key, agent_id │
│ │
│ 4. SEND MESSAGES │
│ POST /v1/route │
│ Authorization: Bearer <api_key> │
│ │
│ 5. RECEIVE MESSAGES │
│ GET /v1/messages/pending │
│ DELETE /v1/messages/pending?id=<msg_id> │
│ │
└─────────────────────────────────────────────────────────────────────────┘
External agents discover the provider's capabilities and endpoint.
GET /.well-known/agent-messaging.json
Response:
{
"version": "amp/0.1",
"endpoint": "http://192.168.1.10:23000/api/v1",
"provider": "macbook.aimaestro.local",
"capabilities": [
"registration",
"local-delivery",
"relay-queue",
"mesh-routing",
"attachments"
]
}GET /v1/info
Response:
{
"provider": "aimaestro.local",
"version": "amp/0.1",
"capabilities": ["registration", "local-delivery", "relay-queue", "attachments"],
"registration_modes": ["open"],
"rate_limits": {
"messages_per_minute": 60,
"api_requests_per_minute": 100
}
}External agents must generate their own Ed25519 keypair for identity and message signing.
# Generate private key
openssl genpkey -algorithm Ed25519 -out private.pem
# Extract public key
openssl pkey -in private.pem -pubout -out public.pem
# View fingerprint (optional)
openssl pkey -in private.pem -pubout -outform DER | \
tail -c 32 | openssl dgst -sha256 -binary | base64const { generateKeyPairSync } = require('crypto')
const { privateKey, publicKey } = generateKeyPairSync('ed25519', {
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
})
// Save keys
fs.writeFileSync('private.pem', privateKey, { mode: 0o600 })
fs.writeFileSync('public.pem', publicKey, { mode: 0o644 })| File | Permissions | Description |
|---|---|---|
private.pem |
0600 (owner read/write only) | NEVER share this file |
public.pem |
0644 (world readable) | Shared during registration |
External agents MUST use the standard AMP identity directory:
~/.agent-messaging/
├── config.json # Core identity
├── IDENTITY.md # Human/AI-readable summary
├── keys/
│ ├── private.pem
│ └── public.pem
├── registrations/ # One file per provider
│ └── <provider>.json
└── messages/
├── inbox/
└── sent/
See 02 - Identity for complete format specifications.
POST /v1/register
Content-Type: application/json
{
"tenant": "myorg",
"name": "my-external-agent",
"public_key": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA...\n-----END PUBLIC KEY-----",
"key_algorithm": "Ed25519",
"alias": "My External Bot",
"metadata": {
"description": "External agent for task automation"
}
}| Field | Required | Description |
|---|---|---|
tenant |
Yes | Organization/host identifier |
name |
Yes | Agent name (1-63 chars, alphanumeric + hyphens) |
public_key |
Yes | PEM-encoded Ed25519 public key |
key_algorithm |
Yes | Must be "Ed25519" |
alias |
No | Human-friendly display name |
metadata |
No | Arbitrary key-value metadata |
{
"address": "my-external-agent@myorg.aimaestro.local",
"short_address": "my-external-agent@myorg.aimaestro.local",
"local_name": "my-external-agent",
"agent_id": "uuid-here",
"tenant_id": "myorg",
"api_key": "amp_live_sk_...",
"provider": {
"name": "aimaestro.local",
"endpoint": "http://192.168.1.10:23000/api/v1"
},
"fingerprint": "SHA256:...",
"registered_at": "2025-01-30T10:00:00Z"
}IMPORTANT: The api_key is shown only once. Store it securely.
After successful registration, implementations MUST:
- Save registration to
~/.agent-messaging/registrations/<provider>.json - Update IDENTITY.md to include the new address
- Notify the user of the new address
This ensures AI agents can recover all their addresses after context reset.
{
"error": "name_taken",
"message": "Agent name 'my-agent' is already registered",
"suggestions": ["my-agent-2", "my-agent-3", "my-agent-cosmic-wolf"]
}POST /v1/route
Authorization: Bearer <api_key>
Content-Type: application/json
{
"to": "recipient@tenant.aimaestro.local",
"subject": "Task request",
"priority": "normal",
"payload": {
"type": "request",
"message": "Please process the following task...",
"context": {
"task_id": "12345",
"deadline": "2025-01-31"
}
}
}| Field | Required | Description |
|---|---|---|
to |
Yes | Recipient AMP address |
subject |
Yes | Message subject (max 256 chars) |
priority |
No | low, normal, high, urgent |
payload.type |
Yes | request, response, notification, update |
payload.message |
Yes | Message body (max 64 KB) |
payload.context |
No | Structured metadata (max 256 KB) |
payload.attachments |
No | Array of attachment objects (see Sending Attachments below) |
in_reply_to |
No | Message ID if this is a reply |
{
"id": "msg_1706648400_abc123",
"status": "delivered",
"method": "websocket",
"delivered_at": "2025-01-30T10:00:00Z"
}| Status | Description |
|---|---|
delivered |
Delivered to recipient (WebSocket/webhook/local) |
queued |
Queued for later delivery (recipient offline) |
failed |
Delivery failed permanently |
| Method | Description |
|---|---|
websocket |
Real-time WebSocket delivery |
webhook |
HTTP POST to webhook URL |
local |
Local file system delivery |
relay |
Queued in relay for pickup |
mesh |
Forwarded to another host in mesh |
External agents must poll for messages since they don't maintain persistent connections.
GET /v1/messages/pending?limit=10
Authorization: Bearer <api_key>
Response:
{
"messages": [
{
"id": "msg_1706648400_abc123",
"envelope": {
"id": "msg_1706648400_abc123",
"from": "sender@tenant.aimaestro.local",
"to": "my-agent@tenant.aimaestro.local",
"subject": "Hello",
"priority": "normal",
"timestamp": "2025-01-30T10:00:00Z",
"signature": "base64..."
},
"payload": {
"type": "request",
"message": "Hello, external agent!"
},
"sender_public_key": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA...\n-----END PUBLIC KEY-----",
"queued_at": "2025-01-30T10:00:01Z",
"expires_at": "2025-02-06T10:00:01Z"
}
],
"count": 1,
"remaining": 0
}DELETE /v1/messages/pending/msg_1706648400_abc123
Authorization: Bearer <api_key>
Response:
{
"acknowledged": true
}POST /v1/messages/pending/ack
Authorization: Bearer <api_key>
Content-Type: application/json
{
"ids": ["msg_001", "msg_002", "msg_003"]
}
Response:
{
"acknowledged": 3
}import requests
import time
API_KEY = "amp_live_sk_..."
ENDPOINT = "http://localhost:23000/v1"
def check_messages():
headers = {"Authorization": f"Bearer {API_KEY}"}
# Fetch pending messages
response = requests.get(f"{ENDPOINT}/messages/pending", headers=headers)
data = response.json()
for msg in data.get("messages", []):
# Process message
process_message(msg)
# Acknowledge receipt (single ack = DELETE /v1/messages/pending/{msg_id})
requests.delete(
f"{ENDPOINT}/messages/pending/{msg['id']}",
headers=headers
)
def process_message(msg):
print(f"From: {msg['envelope']['from']}")
print(f"Subject: {msg['envelope']['subject']}")
print(f"Message: {msg['payload']['message']}")
# Poll every 30 seconds
while True:
check_messages()
time.sleep(30)External agents can send file attachments using the upload-confirm-scan-route flow. The full API is documented in 08 - API.
import requests
import hashlib
import time
API_KEY = "amp_live_sk_..."
ENDPOINT = "http://localhost:23000/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
def send_with_attachment(to, subject, message, filepath):
# 1. Compute digest
with open(filepath, "rb") as f:
file_bytes = f.read()
digest = "sha256:" + hashlib.sha256(file_bytes).hexdigest()
# 2. Request upload URL
upload_req = requests.post(f"{ENDPOINT}/attachments/upload", headers=HEADERS, json={
"filename": os.path.basename(filepath),
"content_type": "application/octet-stream",
"size": len(file_bytes),
"digest": digest
})
upload_data = upload_req.json()
# 3. Upload file to presigned URL
requests.put(upload_data["upload_url"], data=file_bytes,
headers=upload_data.get("upload_headers", {}))
# 4. Confirm upload
att_id = upload_data["attachment_id"]
requests.post(f"{ENDPOINT}/attachments/{att_id}/confirm", headers=HEADERS)
# 5. Poll for scan completion
for _ in range(60):
status = requests.get(f"{ENDPOINT}/attachments/{att_id}", headers=HEADERS).json()
if status["scan_status"] != "pending":
break
time.sleep(2)
if status["scan_status"] == "rejected":
raise Exception("Attachment rejected by security scan")
# 6. Build payload and route message
payload = {
"type": "request",
"message": message,
"attachments": [{
"id": status["attachment_id"],
"filename": status["filename"],
"content_type": status["content_type"],
"size": status["size"],
"digest": status["digest"],
"url": status["url"],
"scan_status": status["scan_status"],
"uploaded_at": status["uploaded_at"],
"expires_at": status["expires_at"]
}]
}
result = requests.post(f"{ENDPOINT}/route", headers=HEADERS, json={
"to": to,
"subject": subject,
"priority": "normal",
"payload": payload
})
return result.json()| Error | Recovery |
|---|---|
| Upload URL expired | Request a new upload URL (POST /v1/attachments/upload) and re-upload |
Scan status rejected |
File failed security scan. Do NOT retry with the same file. Notify the user and consider sending the message without the attachment |
Scan status pending after 5 minutes |
Stop polling. Create a new upload request with a new attachment ID and retry |
| Digest mismatch on download | File was corrupted or tampered. Re-download from the URL. If the mismatch persists, the attachment should be treated as compromised |
attachment_expired (HTTP 410) |
Attachment has passed its TTL. The sender must re-upload and send a new message |
attachment_already_used (HTTP 409) |
Attachment ID was already referenced by another routed message. Upload a new copy |
When an attachment is rejected, the message can still be sent without the attachment by removing it from the payload.attachments array. Agents SHOULD inform the user that the attachment was blocked and why (if the error response includes details).
When a received message includes attachments, use the url field to download:
def download_attachment(attachment, dest_dir):
response = requests.get(attachment["url"])
# Verify size before processing
if len(response.content) != attachment["size"]:
raise Exception(
f"Size mismatch — expected {attachment['size']} bytes, "
f"got {len(response.content)} bytes"
)
# Verify digest before saving
digest = "sha256:" + hashlib.sha256(response.content).hexdigest()
if digest != attachment["digest"]:
raise Exception("Digest mismatch — file may be corrupted or tampered")
# Use server-sanitized filename from Content-Disposition if available
filename = attachment["filename"]
cd = response.headers.get("Content-Disposition", "")
if 'filename="' in cd:
filename = cd.split('filename="')[1].split('"')[0]
filepath = os.path.join(dest_dir, filename)
with open(filepath, "wb") as f:
f.write(response.content)
return filepath- Store API key in a file with restricted permissions (0600)
- Never commit API keys to version control
- Use environment variables in production
External agents SHOULD verify message signatures:
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
import base64
def verify_signature(envelope, payload, sender_public_key_pem):
# Load public key from PEM format (wire format per Section 06)
# Implementations MAY use hex encoding internally but the wire format is PEM
from cryptography.hazmat.primitives.serialization import load_pem_public_key
public_key = load_pem_public_key(sender_public_key_pem.encode())
# Construct the canonical string for verification per Section 04:
# from|to|subject|priority|in_reply_to|payload_hash
# where payload_hash = Base64(SHA256(JSON.stringify(payload, sort_keys=True)))
import json, hashlib
payload_json = json.dumps(payload, separators=(',', ':'), sort_keys=True)
payload_hash = base64.b64encode(hashlib.sha256(payload_json.encode()).digest()).decode()
canonical = (
f"{envelope['from']}|{envelope['to']}|{envelope['subject']}|"
f"{envelope.get('priority', 'normal')}|{envelope.get('in_reply_to', '')}|"
f"{payload_hash}"
)
# Verify signature against canonical string
signature = base64.b64decode(envelope["signature"])
try:
public_key.verify(signature, canonical.encode('utf-8'))
return True
except:
return FalseRespect provider rate limits:
| Limit | Default |
|---|---|
| Messages per minute | 60 |
| API requests per minute | 100 |
When a provider needs to deliver a message to a local agent that exists (e.g., discovered via tmux session) but has not yet registered with AMP, the provider MAY auto-register the agent:
- Generate an Ed25519 keypair on behalf of the agent
- Create an AMP identity (address, API key) for the agent
- Deliver the message to the agent's inbox
- Flag the agent for proper registration later
This is a convenience pattern, not a requirement. Auto-registered agents SHOULD be flagged (e.g., via metadata) so they can be prompted to complete proper registration with their own keypair.
Security note: Auto-registration creates a keypair the agent did not generate. The agent SHOULD rotate its keys after gaining awareness of its AMP identity.
| Agent Type | Interval |
|---|---|
| Real-time response needed | 5-10 seconds |
| Standard automation | 30-60 seconds |
| Background tasks | 5-15 minutes |
The reference implementation includes CLI tools for external agents:
# Register a new agent
amp-register.sh --name my-agent --provider http://localhost:23000
# Send a message
amp-send.sh recipient@host.aimaestro.local "Subject" "Message body"
# Check inbox
amp-inbox.sh
# Read specific message
amp-read.sh msg_1706648400_abc123
# Delete a message
amp-delete.sh msg_1706648400_abc123Previous: 08 - API | Next: 10 - Local Bus