AITP integration
How the playground talks to the AITP SDK, where each protocol moment lives in the codebase, and why the boundaries are drawn the way they are.
The hard rule
All AITP protocol logic — keygen, manifest construction, the handshake
state machine, TCT issuance and verification, delegation, revocation
semantics — lives in the aitp Python SDK (built from
aitp-rs/bindings/aitp-py). Nothing in this repo parses an envelope,
canonicalizes JSON, or signs anything. If a future change makes you want
to, the SDK is what needs the new API.
The repo only imports aitp from inside agent workers
(agents/base/bootstrap.py, agents/base/aitp_server.py,
agents/base/agent_admin.py). The playground service itself never
imports the SDK.
This page documents the playground side only — where and why the SDK is called from this repo. For the SDK call signatures and the protocol semantics behind them, read the authoritative sources instead of relying on the summaries here:
- The
aitpPython API, call by call, with RFC sections + feature flags → aitp-rs · sdk-python.md.- The normative wire protocol → AITP RFCs.
Each RFC reference below links the specific spec; if a summary here and an RFC ever disagree, the RFC wins.
Where the SDK is actually called
| SDK call | Caller | Purpose |
|---|---|---|
aitp.AitpAgent.from_seed(bytes) | agents/base/bootstrap.py | Build the agent identity from a deterministic seed. |
agent.build_manifest(...) | agents/base/bootstrap.py | Construct the AitpManifest JSON served from /.well-known/aitp-manifest. |
agent.new_responder() + process_hello / process_commit | agents/base/aitp_server.py | The responder side of the 4-message handshake. |
agent.new_session() + build_hello, process_hello_ack, complete | agents/base/agent_admin.py (in /admin/initiate-handshake) | The initiator side. |
agent.verify_tct(tct_json, required_grant, expected_audience=...) | agents/base/aitp_server.py (verify_capability_tct) | Per-call authorization on /capabilities/<name>. |
agent.build_delegation(held_tct, delegatee_aid, pk, scope, ttl) | agents/base/agent_admin.py (/admin/delegate) | Mint a DelegationToken from a held TCT. |
aitp.verify_delegation(token_json, my_aid) | agents/base/aitp_server.py (/aitp/delegation/redeem) | Verify a presented DelegationToken before issuing a fresh TCT. |
agent.issue_tct_for_delegatee(verified) | agents/base/aitp_server.py (/aitp/delegation/redeem) | Mint the redeemed TCT bound to the delegatee's key. |
That covers the core v0.1 surface. The post-v0.1 / experimental
surfaces below add a handful more calls — all gated behind
experimental-* Cargo features and detected at runtime
(capabilities.md):
| SDK call | Caller | Purpose |
|---|---|---|
agent.new_session(jwks=…, trust_anchors=…) / agent.new_responder(jwks=…, …) | agent_admin.py, aitp_server.py | OIDC-aware handshake sessions — preload a JwksProvider so OIDC peers can be verified. |
aitp.JwksProvider(...) + aitp.compute_aid_jkt(aid) | agents/base/oidc.py, trust/oidc_issuer.py | Verify OIDC ID tokens; bind a token to the agent's key via the cnf.jkt claim. |
agent.verify_tct_cached(tct_json, grant, store, …) | aitp_server.py (verify_capability_tct) | TCT verification with an aitp.TctStore cache on the hot path. |
agent.build_renewal_request(tct_json) / agent.process_renewal_request(req, …) | agent_admin.py (/admin/renew-tct, /admin/process-renewal) | RFC-AITP-0005 §10 in-band TCT renewal (holder + issuer sides). |
aitp.SessionBundleBuilder(agent) + aitp.verify_session_bundle(env, aid) | agent_admin.py (/admin/export…, /admin/verify-session-bundle) | RFC-AITP-0010 session-bundle export + verify. |
aitp.verify_delegation_experimental_multihop(token, aid) | aitp_server.py (/aitp/delegation/redeem) | RFC-AITP-0011 multi-hop delegation verify (replaces verify_delegation when enabled). |
aitp.AitpAgent.generate(suite=…) + agent.build_manifest(...) | aitp_server.py (/admin/rotate-keys) | RFC-AITP-0007 key rotation — fresh keypair + republished manifest. |
aitp.compute_spki_hash(der) + aitp.SpkiPinVerifier(...) | engine (spki_pin_check step) | SPKI client-cert pin computation + verification. |
Everything else is HTTP plumbing or telemetry. The boundary still holds: no envelope is parsed, canonicalized, or signed outside the SDK.
Identity
Each agent's keypair is derived from a deterministic seed:
seed_hex = SHA256("<org>:<run_id>:<agent_id>") # hosting/identity.py- Same run + same agent_id → same AID across restarts. This is the reason scenarios can re-run cleanly and tests can assert on AIDs.
org: externalagents are derived under a separate namespace, so cross-org scenarios produce AIDs that genuinely look like they come from a different org.
The seed lands in bootstrap.aitp.seed_hex; the worker reads it and
calls aitp.AitpAgent.from_seed(bytes.fromhex(seed_hex)).
Manifest
agent.build_manifest(...) returns the JSON served from
/.well-known/aitp-manifest. The worker passes:
display_namefrom the manifest YAML (spec.aitp.display_name).handshake_endpoint=http://localhost:<port>/aitp/handshake/hello.offered_capsfrom the manifest YAML.ttl_secsfrom the manifest YAML.
The wire schema is owned by the SDK
(RFC-AITP-0003).
The playground only fishes offered_capabilities, handshake_endpoint,
aid, and identity_hint.public_key out of it when constructing requests.
Peer discovery (TrustOrchestrator.resolve_peers)
Resolves {agent_id: {manifest_url, did, source?}} based on the
scenario's spec.trust.discovery. The discovery models themselves
(did:web, registry lookup) are described in the spec's
discovery guide;
the cp_registry request/response contract is the CP's
integration-playground.md.
What follows is how the playground applies them:
static
Default. Returns http://localhost:<port>/.well-known/aitp-manifest
for every peer. Used by most scenarios.
did_web
For each agent that has did_web_host set:
- Build
did:web:<host>(URL-encoded port). - GET
<scheme>://<host>/.well-known/did.json. - Find the
AitpManifestservice entry; append/.well-known/aitp-manifestto itsserviceEndpoint. - On any failure, fall back to localhost.
The DID document itself is served by the agent's AitpServer when
did_web_host was passed in the bootstrap (the cross-cloud scenarios
embed localhost:8101 and friends to keep it on-machine).
cp_registry
For agents marked org: external:
- Query
GET <CP_BASE_URL>/registry/agents?capability=<hint>where the hint is the first workflow capability the runner sees for that agent. - If the CP responds with anything, take the first result's
handshake_endpointand derive the manifest URL. - If the CP is disabled, empty, or fails, fall back to localhost
(
source: "static_fallback").
CP unavailability is never fatal.
Handshake
The 4-message mutual handshake is
RFC-AITP-0004;
the SDK calls that drive it are in
sdk-python.md § Mutual handshake.
What's playground-specific is the HTTP plumbing around those calls —
which /admin and /aitp route carries each message:
Initiator (caller) Responder (callee)
/admin/initiate-handshake POST ----.
\
session = agent.new_session() \
GET peer /.well-known/aitp-manifest ─\─────► 200 manifest JSON
hello = session.build_hello(manifest, grants)
POST /aitp/handshake/hello (hello) ─────────► responder = agent.new_responder()
ack, sid = responder.process_hello(hello)
◄──────── 200 ack + X-Aitp-Session-Id
(stash responder under sid)
commit = session.process_hello_ack(ack, sid)
POST /aitp/handshake/commit (commit) ───────► responder.process_commit(commit)
→ (final_ack, tct_json)
emit handshake.complete (responder)
◄──────── 200 final_ack
tct_json = session.complete(final_ack)
held_tcts[peer_port] = tct_json
emit handshake.complete (initiator)Only the initiator receives a TCT it can present back to the
responder. To run the reverse direction the runner triggers
/admin/initiate-handshake on the other agent. The
_establish_pairwise_trust helper does both directions for every pair
when spec.trust.eager: true.
For scenarios that opt out of eager handshakes, explicit handshake
steps run one direction at a time and can carry requested_grants to
scope the TCT.
TCTs and capability authorization
When a peer call hits /capabilities/<name>, the worker calls
server.verify_capability_tct(tct_json, "<name>"):
# agents/base/aitp_server.py
def verify_capability_tct(self, tct_json, required_grant):
if not tct_json: raise 403 "missing X-AITP-TCT"
tct_obj = json.loads(tct_json)["tct"]
jti = tct_obj.get("jti", "")
if jti and jti in self.revoked_jtis:
raise 403 f"tct revoked: jti={jti}"
declared_audience = tct_obj.get("audience")
return self.agent.verify_tct(
tct_json, required_grant,
expected_audience=declared_audience,
)Two checks:
- Local revocation short-circuit (playground choice). The spec (RFC-AITP-0008) places the revocation check after signature verification so a forged jti can't probe the deny set. The demo checks first — every jti in our deny set was observed via a prior handshake, so the early-out is safe and cheaper here. (See Revocation below.)
- SDK
verify_tct, presented-TCT mode. The playground passes the TCT's own declaredaudienceasexpected_audience— the resource-server check for a TCT a peer presented inX-AITP-TCT. The two verification models (holder-receipt vs presented-TCT) and what the audience asserts are documented in sdk-python.md § TCT verification and RFC-AITP-0005.
Any failure produces a 403. The two parse failures (missing token, malformed JSON) are reported distinctly so debugging is easier.
Held TCTs
Each agent process holds a dict held_tcts: {peer_port: tct_json}
populated by /admin/initiate-handshake and by
/admin/redeem-delegation. The map is module-scoped — all requests
in this process see the same set.
/admin/invoke looks up held_tcts[peer_port] and attaches it as
X-AITP-TCT on the request to /capabilities/<name>. If the held
TCT was revoked or expired, the peer's verify_tct will 403; the
admin router wraps that into {error:true, status_code: 403, body}
so probe steps can observe it without crashing the run.
Delegation (RFC-AITP-0006)
Delegation semantics — scope narrowing, the cnf key binding, redemption —
are RFC-AITP-0006
and sdk-python.md § Delegation.
The playground-specific part is the HTTP choreography across two agents:
Single-hop delegation flow:
delegator (researcher) delegatee (sub-researcher) verifier (writer)
holds TCT_AB issued by
writer for {write.content}
/admin/delegate
build_delegation(TCT_AB,
sub.aid, sub.public_key,
scope=[write.content], ttl)
→ DelegationToken (DT)
── returns DT ──► (DT in hand)
/admin/redeem-delegation
POST DT to writer /aitp/delegation/redeem ──►
verify_delegation(DT, writer.aid)
issue_tct_for_delegatee(verified)
→ TCT_BC bound to sub's key
◄── fresh TCT_BC ────────────────────
held_tcts[writer_port] = TCT_BC
/admin/invoke (writer, write.content)
presents TCT_BC ──────────────────► verify_capability_tct(TCT_BC, "write.content")
✓Playground-relevant notes (the SDK enforces the rules; the playground just sequences the calls):
/admin/delegatefeeds the delegator's held TCT intobuild_delegation; the SDK rejects ascopewider than that TCT's grants, so a scenario can only narrow./aitp/delegation/redeemonly issues if the presenting party matches the token'sdelegator— an agent can't redeem a chain it never authored.- The redeemed TCT lands in the delegatee's
held_tcts[target_port], so the next/admin/invokefrom delegatee → target presents it automatically.
Revocation (RFC-AITP-0008)
Revocation is local to the issuer:
- The runner's
revoke_tctstep finds the jti of the TCTissuergranted toaudienceby walking the event log (ScenarioRunner._find_tct_jti) and POSTs it to the issuer's/admin/revoke-tct. - The issuer adds it to
revoked_jtis(mutates the same setAitpServerconsults). - Subsequent capability calls that present that jti hit the local revocation short-circuit and 403.
By default no revocation list is published over the wire — local deny-set
fail-closed is all the base demo needs
(RFC-AITP-0008
defines the signed-list distribution model). The revoke_tct step's
via_cp: true mode does exercise the full path — publish to the Control
Plane and have the audience pull the CP's signed
/.well-known/aitp-revocation-list; see
control-plane.md.
Post-v0.1 experimental surfaces
Each surface is gated behind an SDK experimental-* Cargo feature and
reported by GET /capabilities; scenarios degrade cleanly when the wheel
lacks one (capabilities.md). The SDK mechanics for all
of these are in
sdk-python.md § Experimental surface;
below is only what the playground wires up and which scenario shows
it.
| Surface | Playground wiring (the part that's ours) | Scenario | Spec |
|---|---|---|---|
| OIDC identity | The engine mints a per-run mock OIDC issuer (trust/oidc_issuer.py) and threads its key material through every bootstrap; OIDC agents sign via an oidc_mint_jwt callback, pinned-key agents still get a JwksProvider to verify OIDC peers. Real deployments swap in an external IdP. | intra-org/oidc-identity (+ p256-suite template) | RFC-AITP-0002 · sdk-python.md |
| Key rotation | /admin/rotate-keys regenerates the key + republishes the manifest; verify_capability_tct's issuer-AID guard then rejects TCTs minted under the old AID before the SDK is consulted. | intra-org/key-rotation | RFC-AITP-0007 |
| TCT renewal | Holder's /admin/renew-tct → issuer's /admin/process-renewal; the holder swaps its held TCT in place. | intra-org/tct-renewal | RFC-AITP-0013 |
| TCT verification cache | When aitp.TctStore exists, verify_capability_tct routes through verify_tct_cached; tct_cache_stats exposes hit/miss counters. | intra-org/tct-cache-perf | RFC-AITP-0005 |
| Session bundles | A coordinator's /admin/export-session-bundle packages the TCTs it issued; a verifier's /admin/verify-session-bundle returns the BundleOutcome. | intra-org/session-bundle | RFC-AITP-0010 |
| Multi-hop delegation | The redeem endpoint swaps verify_delegation for verify_delegation_experimental_multihop when available. | intra-org/delegation-multihop | RFC-AITP-0011 |
| SPKI pinning | A pure-SDK spki_pin_check step — no agent involved. | intra-org/spki-pinning | sdk-python.md |
What you can ignore (boundary check)
If you find yourself wanting to do any of these in this repo, the SDK should be doing it instead:
- Parse a TCT envelope to inspect grants (the runner only reads
jtifor revocation lookup; everything else routes throughverify_tct). - Canonicalize JSON for signing.
- Verify a signature.
- Build any AITP message by hand.
- Track handshake state across multiple requests (the responder map
in
AitpServer._sessionsis keyed bysession_idfrom the SDK, not state we own).
The playground exists to drive scenarios; the SDK exists to enforce the protocol. Keep the wall solid.