Directives
A directive is a small, signed, structured object that frames a privacy-preserving session. Think of directives as the envelopes of an AP3 conversation — they don't carry the cryptographic payloads themselves, but they declare what the parties intend to do, what they did, and what the result was.
There are two directives in AP3 today:
| Directive | Sent by | Purpose |
|---|---|---|
PrivacyIntentDirective |
Initiator | Opens a session. Says: "I want to run this operation, between these participants, by this expiry." |
PrivacyResultDirective |
Initiator (today) | Captures the outcome. Says: "Here's the result of session X, hashed and signed." |
Directives are how AP3 makes the who, when, what, and outcome of a session non-repudiable and auditable, even though the underlying data never leaves its owner.
Why two directives, not one
The lifecycle of a privacy-preserving session has two natural checkpoints:
- Before any computation — both sides need to agree on the operation, participants, expiry, and replay protection. Get this wrong (replay an old request, accept an expired one, run an operation neither side advertises) and the cryptography downstream is meaningless. That's the
PrivacyIntentDirective. - After the protocol completes — exactly one party (in PSI, the initiator) holds the result. To prove later that this result corresponds to this session and not some other one, you need a signed, hash-linked artifact. That's the
PrivacyResultDirective.
Splitting the two lets each be small, simple, and independently auditable.
Privacy Intent Directive
The PrivacyIntentDirective rides on every outbound envelope from the initiator. Each instance binds to the payload of its own envelope via payload_hash, so every initiator→receiver message in a session is independently signed and tamper-evident. It looks like this:
{
"ap3_session_id": "string",
"intent_directive_id": "string",
"operation_type": "PSI",
"participants": ["agent_id_1", "agent_id_2"],
"nonce": "string",
"payload_hash": "sha256_hex",
"expiry": "ISO8601_timestamp",
"signature": "string"
}
Field-by-field
| Field | What it does | Why it matters |
|---|---|---|
ap3_session_id |
Unique identifier for this session. | Anchors all subsequent messages and the eventual result to one logical session. |
intent_directive_id |
Unique identifier for this directive. | Lets you reference this exact intent in logs and dispute resolution. |
operation_type |
The AP3 operation to run (today: "PSI"). |
The receiver checks this against supported_operations in its own AgentCard before accepting. |
participants |
Agent IDs / URLs of all parties. | Receiver verifies that it actually appears in the participant list. |
nonce |
Initiator-chosen random value, fresh per intent. | Replay protection. A receiver that has seen the (intent_id, nonce, payload_hash) triple before must reject. |
payload_hash |
SHA-256 hex of this envelope's protocol payload. | Cryptographically binds the intent to the bytes the receiver is about to process. Any swap is caught with INTENT_PAYLOAD_MISMATCH. |
expiry |
ISO 8601 timestamp after which the directive is invalid. | Stale directives get rejected. Always set a sensible expiry (e.g. 1 hour). |
signature |
Base64 Ed25519 signature over the canonical directive payload. | Tamper-evidence. Verified with PrivacyIntentDirective.verify_signature() against the initiator's published key. |
The receiver runs full validation on the session-opening intent (signature, participants, expiry, payload binding, replay), pins the signer's pubkey, and then on every subsequent intent re-checks signature (against the pinned key), payload binding, and replay. If any check fails, it returns a PrivacyProtocolError (see AP3 A2A Extension) and the session is dead.
Privacy Result Directive
The PrivacyResultDirective captures the outcome of a session. In the current SDK, it is a local artifact — see the note below.
Current SDK behavior (AP3 over A2A)
In the current ap3.a2a implementation, the PrivacyResultDirective is constructed and signed by the initiator
and returned to the initiator's application code after the protocol completes.
It is not sent on-wire as an A2A DataPart today. On-wire messages are carried as ProtocolEnvelope objects in
Part.data and contain operation-specific protocol payloads. Each initiator→receiver envelope additionally carries
a freshly-signed PrivacyIntentDirective bound to that envelope's payload (see Privacy Intent Directive).
If you need a receiver-signed result receipt for compliance/non-repudiation, see the potential improvement below.
Schema
{
"ap3_session_id": "string",
"result_directive_id": "string",
"result_data": {
"encoded_result": "string",
"result_hash": "string",
"metadata": {
"computation_time": "string",
"elements_found": "number"
}
},
"proofs": {
"correctness_proof": "string",
"privacy_proof": "string",
"verification_proof": "string"
},
"signature": "string"
}
Field-by-field
| Field | What it does |
|---|---|
ap3_session_id |
The session this result belongs to. Must match the prior PrivacyIntentDirective. |
result_directive_id |
Unique identifier for this result directive. |
result_data.encoded_result |
The actual outcome (e.g. base64-encoded boolean for PSI). In the current SDK this is encoded, not encrypted — the result is something the initiator already learned. |
result_data.result_hash |
Hash of the result payload, used by verify_integrity() to confirm nothing was tampered with later. |
result_data.metadata |
Free-form metadata about the computation (timing, counts, query reference). |
proofs.* |
Experimental placeholder fields in the current SDK — they exist to exercise the wire format end-to-end. They are not real cryptographic proofs yet. Real proof generation/verification is on the Roadmap. |
signature |
Base64 Ed25519 signature over the canonical payload. Verified with PrivacyResultDirective.verify_signature(). |
A2A DataPart keys
When directives are sent as A2A DataParts, they are keyed by:
ap3.directives.PrivacyIntentDirectiveap3.directives.PrivacyResultDirective
Concrete DataPart examples are in AP3 A2A Extension.
Directive exchange sequence
sequenceDiagram
autonumber
participant I as Initiator
participant R as Receiver
I->>R: PrivacyIntentDirective #1 (binds session-opening envelope)
R-->>I: Validate intent, pin signer key, accept/reject
Note over I,R: Operation-specific cryptographic rounds run here
I->>R: PrivacyIntentDirective #N (binds round-N envelope) + protocol payload
R-->>I: Re-validate (signature, payload_hash, replay), process round
I-->>I: Construct + sign PrivacyResultDirective (initiator-local artifact)
For developers, the practical flow is:
- Initiator builds an intent for every outbound envelope: it sets
payload_hashto the SHA-256 hex of that envelope's payload, calls.sign(), and embeds the intent alongside the payload inPart.data. - Receiver pulls the intent out, calls
validate_directive()andverify_signature()on the first inbound envelope (and pins the signer's pubkey for the session). On every subsequent intent-bearing envelope, it re-verifies the signature against the pinned key plus the per-envelopepayload_hashand replay key. - After the final cryptographic round, the initiator derives the result locally and signs a
PrivacyResultDirectivefor its audit log.
Potential improvement: receiver-signed result receipt (optional)
Some deployments want the receiver to send a signed receipt of the result for audit and compliance. For PSI specifically, the receiver does not compute or learn the final boolean result in the current protocol — the initiator does. So a naïve "receiver signs the result" round doesn't make sense as-is.
To reconcile "receiver acknowledges a result" with PSI's privacy goals, you can add a final attestation round on top of the existing exchange:
- The standard PSI envelopes complete (init → msg0 → msg1 → msg2 for PSI today).
- A → B:
result_claim— initiator's claimed result + transcript hash, signed by the initiator. - B → A: receiver-signed
PrivacyResultDirectivethat binds to the transcript and the claimed result (receiver attests "this result is consistent with the transcript I participated in" — not "this result is correct," since it never learned the result).
This gives non-repudiation ("receiver acknowledges result R for transcript T") without leaking the receiver's private set. Later, real proofs can be added so the receiver — or any third party — can verify correctness of the claimed result without learning private inputs. That work is tracked under Private APIs and the Roadmap.