Architecture
Big picture
The playground is a FastAPI service that owns the run lifecycle. A client starts a run of a named scenario; the scenario drives one or more agents; each agent calls an LLM and publishes signed context to a registry; the registry verifies and stores it, then fires a webhook back to the playground; the playground fans every protocol step out to the client over SSE.
┌──────────────────────────────────────────────┐
client ──POST /runs──▶ playground (FastAPI) │
▲ │ │
│ SSE │ scenario.run(spec, events) │
│ /runs/{id}/events ▼ │
└────────────── in-process SSE bus ◀── webhook.received ──┐ │
│ │ │
▼ agent.publish / retrieve / search │ │
┌──────────────┐ webhook ┌───────────────┴┐ │
│ acdp_client │────────────▶│ registry-a/-b │ │
│ (async httpx)│◀────────────│ (Rust binary) │ │
└──────┬───────┘ POST /ctx └───────┬────────┘ │
│ sign via acdp (Rust SDK) │ │
▼ ▼ │
forward webhooks ───────────▶ control-plane ───┘
run start/complete (optional, NestJS)Components
The playground service (playground/)
| Module | Responsibility |
|---|---|
main.py | FastAPI app, CORS, router wiring, lifespan |
config.py | pydantic-settings over .env; get_settings() is lru_cached |
api/ | HTTP routers: health, scenarios, runs, contexts, webhooks |
scenarios/ | Scenario registry, run lifecycle, and the S1–S21 catalog |
agents/ | BasePlaygroundAgent + LangChain / CrewAI / LangGraph adapters |
events.py | In-process SSE bus — one asyncio.Queue per run |
control_plane.py | Optional fire-and-forget bridge to the control plane |
conformance.py | Live conformance probes against real binaries |
pinned_keys.py | Key-rotation window evaluation (RFC-ACDP-0008 §9.3) |
retry_after.py | RFC 9110 Retry-After parsing (re-export) |
logging_setup.py | pretty / json structured logging |
The client library (acdp_client/)
An async httpx + Pydantic layer over the acdp Rust SDK. It owns transport
and type marshaling; all cryptography, JCS canonicalization, and SSRF IP
classification are delegated to the Rust SDK. See Client SDK.
The SDK (acdp, from acdp-rs/bindings/acdp-py)
A compiled (maturin/pyo3) extension the playground imports for every protocol
primitive — signers (AcdpProducer / AcdpP256Producer), the verifier, the JCS
canonicalizer, the SSRF policy, and the did:web resolver. The playground does
not reimplement any of these; it only orchestrates them. The SDK is its own
project — see
acdp-rs
(producing,
consuming,
security,
bindings).
The run lifecycle
-
POST /runs(api/runs.py) validates the scenario, generates a UUIDrun_id, merges scenario defaults with request inputs into aRunSpec, creates an event queue (events.create_queue), spawnsrunner.execute(...)as a background task, and notifies the control plane of the start. Returns 202 with astream_url. -
runner.execute(scenarios/runner.py) emitsrun.started, calls the scenario'srun(spec, events)coroutine, then emitsrun.complete(withcontexts_producedand thelineage_graph) orrun.error(with a captured traceback). TheRunResultis persisted in an in-process dict and anotify_run_completeis fired to the control plane. -
The scenario uses
_factory.pyhelpers to mint deterministic agent identities, buildAcdpClients (one per registry, cached in anAgentBundle), and run agents. Each agent action (publish,retrieve,search) emits aStepEventonto the queue. -
The registry verifies the signature, stores the context, and (in a fully live deployment) POSTs a webhook to
/webhooks/acdp. The webhook handler verifies the HMAC signature, lifts tenant/dedup/run headers onto the event, transforms it into aStepEvent, and enqueues it onto the matching run's bus. -
GET /runs/{id}/eventsdrains the queue astext/event-stream, emitting keepalives every 15s and terminating onrun.complete/run.error. If the run already finished, it replays the final result instead.
Determinism & identity
Agent identities are deterministic within a run but fresh across runs.
RunSpec.agent_seed(slug) is sha256(run_id:slug), so the same slug always
yields the same 32-byte key seed within a run, while a new run_id produces a
new identity. Producers are minted from these seeds in _factory.producer_for
(P-256 rehashes the seed to a valid curve scalar). DIDs follow
did:web:{authority}:agents:{slug} with key id {did}#key-1.
Graceful degradation
Many V2/security scenarios depend on infrastructure that a stock registry can't
fully provide offline (live token issuance needs web-hosted did:web
documents; some checks need the control plane). These scenarios are built to
degrade gracefully — they complete and mark themselves complete-but-degraded
via a degraded: true flag in the run summary rather than failing. Their
deterministic cores (P-256 crypto, cursor logic, tenant-header policy, rotation
windows, Retry-After) are always exercised offline. See
Scenarios for which scenarios degrade.
The control plane bridge
This describes the playground side of the integration. The control plane is
its own project — its ingest, auth, introspection, revocation, and policy
surfaces are documented in
acdp-control-plane
(API.md,
INGEST.md,
AUTH.md).
When CONTROL_PLANE_URL is empty the playground runs standalone and every
forwarding method is a no-op. When set:
- Every registry webhook is HMAC-signed with the CP secret and forwarded to
/ingest/acdp, preserving theX-ACDP-Event-Iddedup key and stampingX-Tenant-Idfor tenant attribution. - Run start/complete notifications are posted.
- The bridge honors a cooperative
Retry-Afteron transient upstream responses (429/502/503/504) with one capped retry.
When CONTROL_PLANE_ADMIN_TOKEN is set, ControlPlaneClient also drives the CP
operator surface: introspect (RFC 7662), the cross-issuer revocations feed,
the cross-run events history, capability declaration, domain-pack listing, and
reload_pinned_keys.