Build agents behind an authorization gate.
These docs are written for both humans and AI coding assistants. The goal is simple: create agents, store keys safely, sign each consequential action, submit to A2SPA, and proceed only after an authorized receipt is returned.
Showing all sections.
Fastest working path
- Create an account. New accounts may include starter credits when public registration is enabled.
- Create two agents. One sender and one receiver. Download the key bundle immediately.
- Store private keys outside chat. Put private keys in a secret manager, deployment secret, or local file outside source control referenced by an environment variable.
- Wrap the existing agent. Build and sign a payload before the agent requests a consequential tool.
- Call
POST /v1/verify_payload. Only proceed if A2SPA returnssuccess: trueand a receipt decision ofauthorized. - Protect the target. In A2SPA 2.1 deployments, the target, gateway, service, or executor must refuse the final action unless the valid A2SPA authorization artifact reaches the execution boundary.
Tell an AI coding assistant this
Use the A2SPA Integration Pack.
Create or identify sender and receiver agents.
Store the issued private key in my secret store.
Wrap every consequential action so it builds a signed A2SPA payload.
Submit the signed request to POST /v1/verify_payload.
Do not proceed unless A2SPA returns success: true and receipt.receipt.decision == "authorized".
Put the protected target behind an A2SPA 2.1 enforcement guard.
Do not write directly to backend storage.
Do not paste private keys into chat.
How an AI can build this for a customer
The assistant should use the authenticated dashboard for agent setup and the Integration Pack for code generation inside the user's repo.
Dashboard Setup
Used from a signed-in browser session to create agents, manage keys, set policy, and download issued key bundles.
Integration Pack
Used in the customer's codebase to add signing helpers, environment variables, runtime wrapper functions, and polling helpers.
Assistant operating rules
- Ask what actions the user's agents perform: email, calendar, payment, database write, code deployment, browser action, or custom workflow.
- Create one A2SPA agent per runtime identity, not one giant shared agent for everything.
- Map each action to signed
input.action,input.workflow_scope, optionalamount_usd,currency, andon_behalf_of. - Set dashboard policy so the action cannot exceed its intended scope.
- For money, inventory, approvals, or long-running workflows, register state continuity before continuation.
Identity answers who. A2SPA checks what may proceed.
A2SPA should sit immediately before a consequential action. If signature, nonce, timestamp, ownership, policy, or continuity checks fail, the action should not proceed. A2SPA receipts prove verifier decisions, not target-side commit unless your integration adds target acknowledgements.
Every path to consequence must cross the authorization boundary.
A2SPA 2.1 remains a deployment/enforcement profile under the hosted v1.0 contract. A verifier decision does not prove an arbitrary downstream commit unless the integration enforces that boundary.
Protected Target Rule
The target, trusted gateway, service, or executor must fail closed unless the required A2SPA authorization result and signed receipt are valid for the exact target action.
No Alternate Path
The agent may request an action, but it should not hold credentials or routes that can directly commit the protected consequence without the A2SPA gate.
Adversarial tests to run
- Post-check substitution: verify that the object checked is the object committed.
- Nested interpretation and parser differentials: verify that one signed representation has one consequential meaning.
- Referent or alias substitution: verify that names, pointers, queues, and namespaces cannot resolve to a different protected target at commit time.
- Cached authorization: verify that stale allows cannot be reused for a later or altered execution.
- Fail-closed resilience: verify that verifier timeouts, malformed results, and unexpected exceptions do not let the consequence proceed.
Bypass paths to inventory
- Admin endpoints, legacy APIs, undocumented routes, and dashboards.
- Service accounts, internal RPC, queues, workers, cron jobs, and retry paths.
- Direct database writes where the write itself creates the consequence.
- Maintenance and break-glass paths that could commit the same result.
Python target-side guard
import os
from a2spa import SQLiteTargetOperationLedger, execute_protected_action
operation_id = message["idempotency_key"]
execution = execute_protected_action(
a2spa_verify_result,
lambda authorization: {
"provider_message_id": email_provider.send(message).id,
},
target_id="smtp-gateway",
operation_id=operation_id,
target_private_key_pem=os.environ["A2SPA_TARGET_ACK_PRIVATE_KEY_PEM"],
target_agent_id="user-123_email-target",
action="send_email",
workflow_scope="email:send",
trusted_receipt_public_key_pem=os.environ["A2SPA_RECEIPT_PUBLIC_KEY_PEM"],
operation_ledger=SQLiteTargetOperationLedger("target-operations.sqlite3"),
require_target_idempotency=True,
)
SQLiteTargetOperationLedger is for durable single-node targets. Multi-instance targets need a transactional shared-store adapter with the same claim/commit/fail contract. If any route can execute without valid A2SPA authorization at the target boundary, the deployment fails the 2.1 profile.
Constrain the exact action, preserve the evidence, and retire temporary agents.
A2SPA 2.2 foundation work adds granular policy constraints, policy provenance, portable evidence packages, and ephemeral scoped agents while preserving the A2SPA 2.1 target-side enforcement invariant.
Granular Policy
Targets can compare signed a2spa.policy.v1 constraints against the actual action context: resource, environment, workflow, payload hash, parameters, usage, and approval state.
Data Scope
Data access can be narrowed to a dataset, purpose, maximum records, and a deterministic field subset such as requested_fields ⊆ allowed_fields.
Evidence Packages
Auditors can verify signed authorization receipts, policy hashes, target acknowledgements, and trust policy without trusting the sender or target alone.
Ephemeral Agents
Task-scoped agents can carry TTL, parent, task, and max-use metadata, then fail closed after completion or expiration while preserving audit history.
Signed chain validation and the local T01-T16 adversarial matrix are implemented. Production cross-organization use still requires independently reviewed trust anchors, revocation operations, target integration, and deployment evidence.
Validate delegated authority and protect the state that enforcement depends on.
A2SPA now has local SDK foundations for signed delegation-chain validation, deterministic permission attenuation, trust-anchor checks, revocation-status fail-closed behavior, and MAC-protected enforcement-state records.
Delegation Chains
a2spa.delegation.v1 grants bind issuer, subject, audience, permissions, parent grant hash, validity window, child depth, and signatures.
Attenuation
Child grants must explicitly carry every parent constraint forward with equal-or-narrower values. Unknown delegation constraints fail closed.
Trusted Roots
The target supplies trusted root anchors for the audience, tenant, environment, and action family. A grant is evidence, not automatic permission.
State Integrity
a2spa.state_integrity.v1 records use an independently protected HMAC key to detect tamper, rollback, sequence drift, and authorization-state mismatch.
Production rollout still requires deployment-specific trust-anchor storage, revocation freshness policy, atomic budget reservation for cumulative limits, and adversarial evidence before broad cross-organization guarantees are claimed.
Each runtime identity gets an agent key.
A2SPA uses an account API key to authenticate calls and per-agent private keys to sign payloads. Private keys are shown only when issued or rotated.
| Item | Purpose | Where It Lives |
|---|---|---|
| Account API key | Authenticates requests to A2SPA APIs. | Server secret or environment variable. |
| Agent private key | Signs the exact action payload. | Your secret manager. Never chat. |
| Agent public key | Used by A2SPA verifier to check signatures. | Stored by A2SPA. |
| Key bundle | One JSON file containing issued key material. | Downloaded once and stored securely. |
The helper libraries are narrow by design.
A2SPA payload helpers build canonical payloads, compute hashes, sign actions, build policy facts, create state-continuity claims, verify signed evidence, and enforce authorized results at protected Python targets. Verifier authorization decisions remain server-side.
Python package target
pip install a2spa
from a2spa import build_payload_fields, build_signed_requestNode package target
npm install a2spa
const { buildPayloadFields, buildSignedRequest } = require("a2spa");Distributed helper libraries are client-side helpers only. A2SPA server APIs remain the enforcement authority.
Copy-paste code examples
Run these examples in server-side code. Keep A2SPA_API_KEY and the agent private key in environment variables or a secret manager, never in browser code.
Python: sign, verify authorization, then proceed
import json
import os
from pathlib import Path
from urllib import error, request
from a2spa import build_payload_fields, build_signed_request, policy_input, require_authorized_result
base_url = os.getenv("A2SPA_API_BASE", "https://api.aimodularity.com/A2SPA").rstrip("/")
api_key = os.environ["A2SPA_API_KEY"]
private_key_text = Path(os.environ["A2SPA_PRIVATE_KEY_PATH"]).read_text(encoding="utf-8")
payload = build_payload_fields(
agent_id=os.environ["A2SPA_AGENT_ID"],
target_agent_id=os.environ["A2SPA_TARGET_AGENT_ID"],
input_data=policy_input(action="send_email", workflow_scope="email:send"),
output_data={"status": "ready"},
)
request_body = build_signed_request(private_key_text, payload)
http_request = request.Request(
f"{base_url}/v1/verify_payload",
data=json.dumps(request_body).encode("utf-8"),
headers={"Content-Type": "application/json", "x-api-key": api_key},
method="POST",
)
try:
with request.urlopen(http_request, timeout=15) as response:
result = json.loads(response.read().decode("utf-8"))
except error.HTTPError as exc:
result = json.loads(exc.read().decode("utf-8") or "{}")
raise RuntimeError(result.get("error", f"A2SPA denied the action ({exc.code})")) from exc
require_authorized_result(result)
receipt_id = result["receipt"]["receipt"]["receipt_id"]
print(f"A2SPA authorized the action. Receipt: {receipt_id}")
# Caller-side continuation may proceed only after this point.
# Target commit and exactly-once effects still depend on integration idempotency.
Node: sign, verify authorization, then proceed
const fs = require("node:fs");
const { buildPayloadFields, buildSignedRequest, policyInput, requireAuthorizedResult } = require("a2spa");
async function main() {
const baseUrl = (process.env.A2SPA_API_BASE || "https://api.aimodularity.com/A2SPA").replace(/\/$/, "");
const privateKeyPem = fs.readFileSync(process.env.A2SPA_PRIVATE_KEY_PATH, "utf8");
const payload = buildPayloadFields({
agentId: process.env.A2SPA_AGENT_ID,
targetAgentId: process.env.A2SPA_TARGET_AGENT_ID,
input: policyInput({ action: "send_email", workflowScope: "email:send" }),
output: { status: "ready" },
});
const requestBody = buildSignedRequest(payload, privateKeyPem);
const response = await fetch(`${baseUrl}/v1/verify_payload`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.A2SPA_API_KEY,
},
body: JSON.stringify(requestBody),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.reason_code || result.error || `A2SPA did not authorize delivery (${response.status})`);
}
requireAuthorizedResult(result);
const receiptId = result.receipt.receipt.receipt_id;
console.log(`A2SPA authorized the action. Receipt: ${receiptId}`);
// Caller-side continuation may proceed only after this point.
// Target commit and exactly-once effects still depend on integration idempotency.
}
main().catch((error) => {
console.error(error.message);
process.exit(1);
});
Signed request shape
{
"payload": {
"agent_id": "user-123_sender",
"target_agent_id": "user-123_receiver",
"timestamp": "2026-08-10T20:00:00Z",
"nonce": "unique-per-action",
"input": {"action": "send_email", "workflow_scope": "email:send"},
"output": {"status": "ready"},
"alert_threshold": 10,
"crypto_profile": "a2spa.classical.rsa_pkcs1v15_sha256.v1",
"hash": "client-computed-sha256"
},
"signature": "hex-encoded-agent-signature"
}Authorized response shape
{
"success": true,
"receipt": {
"receipt": {
"receipt_id": "rct_example",
"decision": "authorized"
},
"canonical_hash": "receipt-canonical-sha256"
}
}Blocked governance response shape
{
"success": false,
"decision": "refer_hold",
"delivery": {"status": "blocked"},
"reason_code": "AUTHORITY_CONTINUITY_UNRESOLVED",
"receipt": {
"receipt": {
"receipt_id": "rct_example",
"decision": "refer_hold"
}
}
}Receipt lookup
curl https://api.aimodularity.com/A2SPA/v1/receipts/$RECEIPT_ID \
-H "x-api-key: $A2SPA_API_KEY"
What gets signed
The public term is signature profile. The stable wire field remains crypto_profile.
{
"agent_id": "sender-agent-id",
"target_agent_id": "receiver-agent-id",
"timestamp": "2026-08-10T20:00:00Z",
"nonce": "unique-per-action",
"input": {
"action": "reserve",
"workflow_scope": "payments:reserve",
"amount_usd": "100.00",
"currency": "USD"
},
"output": {"status": "ready"},
"alert_threshold": 10,
"crypto_profile": "a2spa.classical.rsa_pkcs1v15_sha256.v1"
}
Hash exactly what A2SPA verifies.
A2SPA signs a stable subset of the payload. Clients should build the payload, compute the hash over the canonical signable fields, then sign that same pre-hash payload. Do not hash transport wrappers, HTTP headers, or local debug fields.
| Included Field | Why It Matters |
|---|---|
agent_id | Binds the sender runtime identity. |
target_agent_id | Binds the intended receiver or execution boundary. |
timestamp | Prevents stale payloads from being accepted outside the verifier window. |
nonce | Supports one-time replay protection. |
input and output | Bind the action facts and expected downstream result. |
alert_threshold | Preserves client-side low-balance warning intent. |
crypto_profile | Selects the signature profile; required for non-legacy profiles and included by current helpers. |
state_continuity | Optional one-time continuation claim. |
Use payload helpers unless you are intentionally implementing the canonicalization contract yourself.
Signed Bytes V1 Compatibility Profile
signable = {
agent_id,
target_agent_id,
timestamp,
nonce,
input,
output,
alert_threshold defaulting to 10,
crypto_profile when present,
state_continuity when present
}
canonical_string = Python json.dumps(signable, sort_keys=True)
signed_bytes = canonical_string encoded as UTF-8
hash = SHA-256(signed_bytes)
signature = crypto_profile algorithm over signed_bytes
hash and signature fields are excluded from signed_bytes.
Current helper compatibility uses Python-compatible JSON bytes. Do not substitute a different JSON canonicalization rule unless A2SPA publishes a new crypto profile for it.
Use /v1 for all new integrations.
The previous /api verification, receipt, continuity, inbox, and log routes remain direct compatibility aliases. Alias responses include an RFC 9745 Deprecation header. No removal date is currently announced; removal requires a published migration path and at least 180 days' notice except for an urgent security or legal change.
Payload signing bytes, API-key authentication, and decision handling are unchanged by the path migration. Update the URL and continue to require success: true with receipt decision authorized.
POST /v1/verify_payload
Call this before downstream delivery or caller-side continuation proceeds. A2SPA checks API key ownership, sender key, target agent, signature profile, hash, signature, timestamp, nonce, policy, credits, and optional state continuity.
curl -X POST https://api.aimodularity.com/A2SPA/v1/verify_payload \
-H "Content-Type: application/json" \
-H "x-api-key: $A2SPA_API_KEY" \
-d @signed-request.json
Policy tells A2SPA what an agent may do.
Put policy-relevant facts in signed input. Then set a policy that allows only those facts. If a signed payload asks for an unapproved action, scope, spend amount, currency, or delegation principal, A2SPA denies before nonce consumption.
Email Agent
{
"allowed_actions": ["send_email", "draft_email"],
"allowed_workflow_scopes": ["email:send", "email:draft"]
}Payment Reservation Agent
{
"allowed_actions": ["reserve"],
"allowed_intents": ["payment_reservation"],
"allowed_workflow_scopes": ["payments:reserve"],
"allowed_currencies": ["USD"],
"max_spend_usd": 100,
"require_state_continuity_for_spend": true
}Use policy as an execution contract, not a label.
Strict policies should allow only the facts the agent is expected to sign. For consequential actions, require action, workflow scope, amount, currency, and state continuity where appropriate.
Outbound Sender Policy
Limit what the sender may request: allowed targets, actions, workflow scopes, spend ceilings, currencies, and delegation principals.
Inbound Receiver Policy
Limit what the receiver will accept: allowed senders, accepted actions, inbound spend ceilings, accepted scopes, and approved intents.
{
"allowed_targets": ["user-123_fulfillment-agent"],
"allowed_actions": ["reserve"],
"allowed_intents": ["payment_reservation"],
"allowed_workflow_scopes": ["payments:reserve"],
"allowed_currencies": ["USD"],
"max_spend_usd": 100,
"require_state_continuity_for_spend": true,
"delegation": {
"enabled": true,
"allowed_principals": ["finance-team"]
}
}
Lock the truth a workflow depends on.
Use state continuity when an agent action depends on something still being true: money still reserved, inventory still available, approval still valid, a ticket still open, or a workflow step not expired. Current state-continuity records bind state to the sender, receiver, action, workflow scope, and optional spend facts by default.
POST /v1/continuity_state
x-api-key: $A2SPA_API_KEY
{
"state_id": "payment-123-reservation",
"state": {"reserved_amount": "100.00", "currency": "USD", "status": "reserved"},
"binding": {
"agent_id": "user-123_payment-agent",
"target_agent_id": "user-123_fulfillment-agent",
"action": "reserve",
"workflow_scope": "payments:reserve",
"amount_usd": "100.00",
"currency": "USD"
},
"sequence": 1,
"status": "active",
"expires_at": "2026-08-10T20:00:00Z"
}
A verified sequence is one-time. Register the next sequence before the next continuation step.
Proof for processed decisions
Processed requests can produce and store an authorization/delivery receipt containing sender, target, payload hash, nonce, timestamp, verifier metadata, policy checks, signature profile, continuity checks, delivery status, and a canonical receipt hash. A receipt decision may be authorized, refer_hold, or denied; downstream delivery should only occur when the decision is explicitly authorized. This receipt is not proof of target-side execution or commit unless your integration adds A2SPA 2.1 target acknowledgement semantics.
Retrieve stored receipts with GET /v1/receipts/<receipt_id> from an authenticated session or with the account API key.
curl https://api.aimodularity.com/A2SPA/v1/receipts/$RECEIPT_ID \
-H "x-api-key: $A2SPA_API_KEY"
Operational control plane
- Create agents, enable/disable them, rotate keys, revoke keys, and delete stale agents.
- Reveal/copy only newly issued API keys. Stored keys are hashed, so rotate to issue a visible key.
- Set policy using presets or JSON.
- Register state continuity records for workflows that need continuation checks.
- Review verified logs, expand payload details, export CSV, and inspect error logs.
Common denials
| Reason | Fix |
|---|---|
| Missing API key | Send x-api-key with the account API key. |
| Hash mismatch | Use helper canonicalization and sign before adding hash. |
| Nonce replay | Generate a fresh nonce for every action. |
| Policy denied | Confirm signed input facts match the sender and receiver policy. |
| State continuity failed | Register current state first and include a matching claim. |
| Insufficient credits | Add credits in the dashboard billing section. |