Scenarios

A scenario is a declarative YAML that names the agents, the trust discovery mode, and a sequence of workflow steps. The registry walks scenarios/ on startup and exposes each scenario by its ref: <pack>/<scenario>@<version>.

Layout on disk

scenarios/
├── _shared/
│   └── agents/                        # Agent manifests reused across scenarios
│       ├── researcher.yaml
│       ├── researcher-extended.yaml
│       ├── writer.yaml
│       └── analyzer.yaml
├── intra-org/
│   ├── pack.yaml                      # Pack metadata (slug, name, tags)
│   ├── research-and-write/
│   │   ├── 1.0.0/
│   │   │   ├── scenario.yaml
│   │   │   └── templates/             # Named variants — see "Template variants"
│   │   │       ├── trust-strict.yaml
│   │   │       └── revoking.yaml
│   │   └── 1.1.0/scenario.yaml
│   ├── scoped-capabilities/1.0.0/scenario.yaml
│   ├── delegation-chain/1.0.0/scenario.yaml
│   ├── revocation-demo/1.0.0/scenario.yaml
│   └── trust-gate/1.0.0/scenario.yaml
├── cross-cloud/
│   ├── pack.yaml
│   └── distributed-review/1.0.0/scenario.yaml
└── cross-org/
    ├── pack.yaml
    └── federated-analysis/1.0.0/scenario.yaml

Loader rules (registry/loader.py):

  • Directories starting with _ are ignored at the pack level — that's how _shared/ is hidden from pack discovery.
  • A pack needs pack.yaml; missing it is logged and the pack is skipped.
  • Inside a pack, every <scenario>/<version>/scenario.yaml becomes one scenario version. The ref is built from the file's own metadata (pack, scenario, version), not from the path, so be consistent between path and metadata or the ref will surprise you.
  • A templates/ directory next to scenario.yaml holds named variant overrides (kind: ScenarioTemplate). Files without that kind are skipped with a warning — they're not loaded as scenarios or as templates.

!include is supported in any YAML loaded by the registry, resolved relative to the including file. Use it to keep big scenarios readable or to share input snippets.

Pack file

apiVersion: aitp.dev/v1
kind: ScenarioPack
metadata:
  slug: intra-org           # used in scenario refs as the prefix
  name: Intra-Org Scenarios
  description: Agents within the same organisation
  tags: [intra-org, same-trust-domain]

Agent manifest

A manifest is reusable across scenarios. It declares the framework, how to launch the worker, and what AITP capabilities the agent offers.

apiVersion: aitp.dev/v1
kind: AgentManifest
metadata:
  id: researcher
  name: Research Analyst
  framework: crewai             # crewai | langchain | langgraph | custom
  version: 1.0.0
spec:
  entrypoint:
    type: python_module         # or python_file
    value: researcher.main
  host:
    python: python3             # optional override; falls back to AGENT_PYTHON
    cwd: agents/researcher      # relative to project root, or absolute
    startupTimeoutMs: 30000
    env:                        # optional, merged on top of inherited env
      MY_VAR: "value"
  aitp:
    offered_caps: [research.query]
    display_name: Research Analyst
    identity_type: pinned_key   # pinned_key (default) | oidc
    # When identity_type: oidc, also set:
    #   oidc_issuer: https://idp.example/    # OIDC issuer URL
    #   oidc_subject: researcher             # subject claim
    signing_suite: ed25519      # ed25519 (default) | p256
    ttl_secs: 3600
  did_web: false

identity_type: oidc builds the manifest with an OIDC identity hint and makes the agent mint ID tokens via the per-run mock issuer at handshake time — see intra-org/oidc-identity and aitp-integration.md. signing_suite: p256 selects the ECDSA suite instead of Ed25519.

Key behaviors:

  • entrypoint.type=python_module becomes python3 -m researcher.main; python_file becomes python3 path/to/file.py. See hosting/adapters/base.py.
  • host.cwd is resolved relative to the project root (parent of scenarios_dir). Absolute paths pass through.
  • Adapters set PYTHONPATH to include agents/base (for the shared modules) and agents/ (for <package>.main imports). Don't add these yourself.
  • aitp.offered_caps is what shows up on the manifest the agent serves, and it's what the runner uses to route capability calls.
  • did_web is informational on the manifest itself; the runtime decision to expose /.well-known/did.json comes from the scenario's per-agent did_web_host (see below).

Scenario file

apiVersion: aitp.dev/v1
kind: ScenarioVersion
metadata:
  pack: intra-org
  scenario: research-and-write
  version: 1.0.0
  name: Research and Write
  summary: >
    Two-sentence pitch shown in /scenarios listings.
  tags: [research, writing, crewai, langchain]

spec:
  inputs:
    schema:                       # JSON Schema; validated by ScenarioRunner
      type: object
      properties:
        topic:   { type: string, default: "AI agent collaboration" }
        style:   { type: string, enum: [academic, casual, technical], default: casual }
      required: [topic]

  agents:
    - id: researcher              # logical ID inside this scenario
      ref: _shared/agents/researcher  # path under scenarios_dir, no .yaml suffix
      port_offset: 0              # AGENT_BASE_PORT + offset
      org: internal               # internal | external (affects seed namespace)
      cloud: aws-us-east          # informational; never used by the runner
      did_web_host: localhost:8101 # set this to expose did:web for this agent
      signing_suite: p256         # optional per-agent override of the manifest's suite

  trust:
    boundary: intra_org           # intra_org | cross_org | cross_cloud
    discovery: static             # static | did_web | cp_registry
    eager: true                   # run pairwise handshakes before workflow

  workflow:
    steps:
      - id: trust
        description: All agents perform AITP mutual handshake
      - id: research
        agent: researcher
        capability: research.query
        input_template: "{{ inputs.topic }}"
      - id: write
        agent: writer
        capability: write.content
        input_from: research

The Pydantic models are in src/aitp_playground/registry/models.py; they are the canonical schema. Anything the YAML carries that isn't defined there is ignored.

Workflow steps

Step types are a playground construct — they orchestrate the protocol, they don't define it. Where a step exercises a protocol behavior the table cites the RFC by number; those are in the AITP RFC index, and how the playground drives each one is in aitp-integration.md.

A step's type defaults sensibly when omitted:

  • Has agent + capabilityworkflow.
  • Otherwise → meta (skipped; useful for narration in the event log).
typeRequired fieldsWhat it does
workflow (default)agent, capabilityRoutes to the agent that offers capability; if agent itself offers it, executes locally via /admin/self-execute. Otherwise calls cross-agent via /admin/invoke.
handshakeinitiator, responder, optional requested_grantsRuns one direction of the AITP handshake. Does not auto-mirror.
capability_call_no_trustagent, target_agent, capability, optional expect_statusPOST directly to the target's /capabilities/<name> with no TCT — used to observe the 403.
capability_probeagent, target_agent, capability, optional expect_statusInvoke via /admin/invoke and inspect the returned status; doesn't fail the run on a non-2xx.
delegatedelegator, delegatee, via_peer, scope, optional ttl_secsdelegator issues a DelegationToken to delegatee using the TCT it received from via_peer.
redeem_delegationdelegatee, target, via_delegationdelegatee POSTs the prior step's token to target's /aitp/delegation/redeem; receives a fresh TCT bound to its own key.
revoke_tctissuer, audience, optional via_cp, optional reasonWalks the event log to find the most recent TCT issuer granted to audience, then POSTs its jti to issuer's /admin/revoke-tct. When via_cp: true, also POSTs the jti to the CP's /api/revocation/entries and asks the audience to pull the updated signed list from /.well-known/aitp-revocation-list — see intra-org/revocation-via-cp.
rotate_keysagentThe named agent replaces its keypair, rebuilds its manifest under the new AID, and clears in-flight handshake sessions. Subsequent capability calls that present TCTs issued under the old AID are rejected by verify_capability_tct's issuer-AID guard — see intra-org/key-rotation.
enroll_with_cpagentThe named agent posts its current manifest to the Control Plane's /api/registry/enroll to mint a one-time bearer token, then re-posts to /api/registry/agents with that token to register. When CP_BASE_URL is unset the step skips. See intra-org/external-enrollment.
cp_subscribe_webhookoptional eventsRegister a webhook on the Control Plane whose URL points back at this run's POST /webhooks/cp/{run_id} receiver. CP returns the webhook id and a secret; the playground stores the secret on the run record so subsequent deliveries can be HMAC-verified (X-Aitp-Signature: sha256=<hex>). events: [] (or omitted) subscribes to every deliverable CP type. When CP_BASE_URL is unset the step skips. See intra-org/webhook-subscription.
renew_tctagent (holder), via_peer (issuer)RFC-AITP-0005 §10 in-band TCT renewal. Holder POSTs to its own /admin/renew-tct with the issuer's port; the holder calls AitpAgent.build_renewal_request against its held TCT, hands the request to the issuer's /admin/process-renewal, which calls process_renewal_request and returns a fresh TctEnvelope. The holder swaps its held TCT in-place. Gated by the SDK's experimental-renewal Cargo feature. See intra-org/tct-renewal.
export_session_bundlecoordinator, participantsRFC-AITP-0010 session-bundle issuance. The coordinator (responder side of prior handshakes) packages the TCTs it has issued to each participant into a SessionBundleEnvelope signed under its own key. Output includes bundle_envelope for downstream verify_session_bundle steps. Gated by experimental-bundle. See intra-org/session-bundle.
verify_session_bundleverifier, via_stepVerify a previously-exported bundle. The verifier's /admin/verify-session-bundle calls the SDK's verify_session_bundle and returns a BundleOutcome ({kind: clear|degraded, active_aids, dropped_aids}). Gated by experimental-bundle.
spki_pin_checkcert_der_b64, pins, optional expect_statusPure-SDK exercise of compute_spki_hash + SpkiPinVerifier. Computes the SHA-256 over the given leaf cert's SubjectPublicKeyInfo and asserts is_pinned matches expect_status (1 = pin must match, 0 = must not match). Gated by experimental-pinning. See intra-org/spki-pinning.
tct_cache_statsagentRead the named agent's RFC-AITP-0005 verification-cache counters ({enabled, hits, misses, size}) and emit tct.cache.stats. Pair with repeated capability calls to show hot-path cache hits. Gated by the SDK's TctStore (tct_cache feature). See intra-org/tct-cache-perf.
cp_provision_trust_anchoragent, optional namespace, optional issuer_urlPush the agent's pinned Ed25519 key (and, when issuer_url is set, an OIDC issuer trust anchor) to the Control Plane under namespace (defaults to the pack slug), then reads both back. When CP_BASE_URL is unset the step skips. See intra-org/cp-trust-anchor-provisioning.
cp_delegation_treeagentWalk the named delegator's delegation chain as the Control Plane observed it (GET /api/delegations with a recursive root_jti query) and emit cp.delegation.tree. When CP_BASE_URL is unset the step skips. See intra-org/cp-delegation-tree.
metaNo-op; records step.skipped.

Fault injection

Any handshake, workflow, or capability_probe step can carry a fault: block. The runner mutates the call's target before issuing it so the step exercises a failure path, and records the outcome as {fault_injected: true, kind, target, error} in step_outputs without raising the run — downstream steps can branch on the result.

- id: doomed_call
  type: capability_probe
  agent: researcher
  target_agent: writer
  capability: write.content
  fault:
    kind: peer_offline
    note: "writer's port is rewritten to an unbound port"

Supported fault.kind values:

KindWhat it does
manifest_404Rewrites the targeted peer's manifest URL to a path that 404s. Use to demonstrate "peer is unreachable for discovery" on a handshake or delegate step.
peer_offlineRewrites the targeted peer's port to a closed port. Use to demonstrate transport-level connection failures on handshake or workflow steps.

See intra-org/fault-injection for a worked example. New events: step.fault_injected, step.fault_complete.

Input plumbing

Each workflow step gets exactly one input:

  • input_from: <prior_step_id> — use that step's output verbatim. This is how data flows from researchwriteanalyse.
  • input_template: "..." — string with {{ inputs.<key> }} placeholders. Only flat substitution against the scenario inputs.
  • Neither → the full inputs dict.

Capability routing

ScenarioRunner._find_capability_holder is the lookup:

  1. If step.agent itself offers step.capability, use it (self-execute).
  2. Otherwise pick the first agent in scenario.spec.agents that offers it.

prefer=step.agent exists to keep agent: X, capability: Y as a self-execute when X actually offers Y — otherwise a scenario like delegation-chain (where both researcher and sub-researcher offer research.query) would silently route to the wrong agent. If you write a scenario with multiple holders, name the one you want via agent:.

Trust eager flag

Eager handshakes (spec.trust.eager: true, the default) run a bidirectional handshake between every pair of agents before any workflow step. After this, every agent holds a TCT for every other agent.

Set eager: false when the scenario itself demonstrates trust gating or scoped grants — trust-gate, scoped-capabilities, revocation-demo, and delegation-chain all do this. Those scenarios control timing and scope themselves via explicit handshake steps.

Template variants

A scenario version can ship named templates under <pack>/<scenario>/<version>/templates/<name>.yaml. A template is a named override on top of the base scenario — same scenario_ref, different posture. Use templates when two demonstrations share participants and inputs but diverge on trust posture or workflow shape.

# scenarios/intra-org/research-and-write/1.0.0/templates/trust-strict.yaml
apiVersion: aitp.dev/v1
kind: ScenarioTemplate
metadata:
  name: trust-strict
  summary: probe a 403 before the explicit handshake
spec:
  trust:
    eager: false       # field-level patch — boundary/discovery fall through
  workflow:
    steps:             # full replacement of base workflow.steps
      - id: probe_no_tct
        type: capability_call_no_trust
        ...

Merge rules:

  • trust patches the base field-by-field (e.g. flip eager without restating boundary).
  • agents and workflow.steps are full replacements. Partial list patching is intentionally not supported — the merge stays deterministic and easy to read.
  • Anything the template omits falls through unchanged.

The registry indexes templates and exposes them in three places:

  • GET /scenarios/<pack>/<scenario>@<version> includes a templates: [{name, summary}, ...] list.
  • GET /scenarios/<pack>/<scenario>@<version>/templates lists them.
  • GET /scenarios/<pack>/<scenario>@<version>/templates/<name> returns the resolved (merged) scenario.

Run a templated scenario via POST /runs:

{
  "scenario_ref": "intra-org/research-and-write@1.0.0",
  "template": "trust-strict",
  "inputs": {"topic": "AI agents"}
}

The CLI surfaces them too:

# Lists available templates at the bottom of base dry-run output
uv run python -m aitp_playground.cli dry-run intra-org/research-and-write@1.0.0 \
  --inputs '{"topic":"test"}'

# Merge a template and print the resolved plan
uv run python -m aitp_playground.cli dry-run intra-org/research-and-write@1.0.0 \
  --template trust-strict --inputs '{"topic":"test"}'

lint walks each template and applies the same checks as the base scenario (agent / capability refs, step graph) against the merged output.

Validating

# Validate everything under scenarios_dir
uv run python -m aitp_playground.cli validate

# Validate a specific pack or scenario
uv run python -m aitp_playground.cli validate scenarios/intra-org
uv run python -m aitp_playground.cli validate scenarios/intra-org/research-and-write

The validator runs Pydantic over every scenario and agent manifest and reports ok or FAIL <path>: <pydantic error>. It does not exercise any runtime behavior — for that, use:

uv run python -m aitp_playground.cli dry-run intra-org/research-and-write@1.0.0 \
  --inputs '{"topic":"test"}'

dry-run adds schema validation against the scenario's inputs.schema and prints the agent/capability layout.

Worked example — adding a scenario

Goal: a new intra-org scenario where the writer asks the researcher to review what it wrote.

  1. Pick a slot: scenarios/intra-org/writer-asks-back/1.0.0/scenario.yaml.

  2. Reuse existing agent manifests — researcher and writer are in _shared/agents/.

  3. Write the YAML:

    apiVersion: aitp.dev/v1
    kind: ScenarioVersion
    metadata:
      pack: intra-org
      scenario: writer-asks-back
      version: 1.0.0
      name: Writer Asks Back
      summary: Writer drafts; researcher critiques the draft.
    
    spec:
      inputs:
        schema:
          type: object
          properties:
            topic: { type: string, default: "AI agent identity" }
          required: [topic]
    
      agents:
        - id: researcher
          ref: _shared/agents/researcher
          port_offset: 0
        - id: writer
          ref: _shared/agents/writer
          port_offset: 1
    
      trust:
        boundary: intra_org
        discovery: static
    
      workflow:
        steps:
          - id: research
            agent: researcher
            capability: research.query
            input_template: "{{ inputs.topic }}"
          - id: write
            agent: writer
            capability: write.content
            input_from: research
          - id: critique
            agent: researcher
            capability: research.query
            input_from: write
  4. dry-run it, then POST /runs and check the event log.

If you need a new capability, you'll also touch an agent worker — see agents.md.

Scenarios in the box

RefWhat it shows
intra-org/research-and-write@1.0.0The simplest happy path.
intra-org/research-and-write@1.1.0Same + an analyzer at the end.
intra-org/trust-gate@1.0.0A capability call with no TCT is rejected; then it succeeds after handshake.
intra-org/scoped-capabilities@1.0.0Grant intersection — TCT scoped to one of two offered caps.
intra-org/revocation-demo@1.0.0RFC-AITP-0008: revoke a TCT's jti; subsequent calls 403.
intra-org/revocation-via-cp@1.0.0RFC-AITP-0008 federation: revocation propagates through the CP's signed /.well-known/aitp-revocation-list.
intra-org/delegation-chain@1.0.0RFC-AITP-0006: single-hop delegation + redeem.
intra-org/delegation-multihop@1.0.0RFC-AITP-0011: two-hop chain (researcher → sub-researcher → analyst).
intra-org/key-rotation@1.0.0RFC-AITP-0007: writer rotates keys; pre-rotation TCTs become invalid.
intra-org/fault-injection@1.0.0Operator-injected manifest_404 and peer_offline faults; run continues with structured failure outcomes.
intra-org/external-enrollment@1.0.0Agent self-enrolls via POST /api/registry/enroll then POST /api/registry/agents with the issued bearer token.
intra-org/webhook-subscription@1.0.0Playground registers a CP webhook and CP fans handshake.complete / other audit events back via POST /webhooks/cp/{run_id}; inspect deliveries with GET /runs/{id}/cp-deliveries.
intra-org/cp-delegation-tree@1.0.0RFC-AITP-0006 delegation observed through the CP: delegate + redeem, then walk the chain via GET /api/delegations.
intra-org/cp-trust-anchor-provisioning@1.0.0Push an agent's pinned key + OIDC issuer to the CP as trust anchors, then handshake/write against them.
intra-org/tct-cache-perf@1.0.0RFC-AITP-0005 verification cache: repeated capability calls hit the SDK's TctStore; tct_cache_stats shows hits vs. misses.
intra-org/oidc-identity@1.0.0RFC-AITP-0002 OIDC identity binding — researcher mints a JWT signed by the per-run mock issuer; writer verifies via the SDK's JwksProvider. Template variant p256-suite runs the same flow with a P-256 researcher (cross-suite).
intra-org/tct-renewal@1.0.0RFC-AITP-0005 §10 in-band TCT renewal: handshake → call → renew → call-with-new-jti.
intra-org/session-bundle@1.0.0RFC-AITP-0010 session trust bundle export + verify. Coordinator-issued TCTs packaged into a signed envelope; verifier returns BundleOutcome.
intra-org/spki-pinning@1.0.0Pure-SDK demo of compute_spki_hash + SpkiPinVerifier against a hard-coded self-signed cert.
cross-cloud/distributed-review@1.0.0Three agents; did:web discovery.
cross-org/federated-analysis@1.0.0CP-registry discovery for an external agent (falls back to static).

Each is a useful template — copy the closest one and edit.