HARP Protocol Specification
1. Overview
Section titled “1. Overview”HARP (Human Approval Request Protocol) is a three-party, end-to-end encrypted protocol for human authorization of AI agent actions. It defines how an Agent Platform sends encrypted requests through a zero-knowledge Relay Service to a HARP App on the user’s mobile device.
The relay is a zero-knowledge dumb pipe. It routes encrypted envelopes by pair_id and never sees any plaintext action data, parameters, reasoning, decisions, or form submissions. All sensitive content is encrypted before it leaves the agent platform and can only be decrypted by the HARP App using the shared key established during pairing.
HARP v1 introduces three intent types (AUTHORIZE, COLLECT, INFORM), severity levels, assurance levels, a relay state machine with webhook delivery, and audit persistence.
2. Architecture
Section titled “2. Architecture”2.1 Three-Party Model
Section titled “2.1 Three-Party Model”Agent Platform
Any system running AI agents — Claude Code, OpenAI agents, custom orchestration systems, or any software that needs human sign-off before taking an action. The platform integrates via the HARP SDK or MCP server. It holds a platform keypair and a per-pairing shared secret derived during the pairing flow. It retains full plaintext audit logs locally and is the only party (besides the user) that ever sees plaintext request content.
Relay Service
A zero-knowledge routing service. It receives encrypted envelopes from the agent platform, looks up the routing table (pair_id → device_id → push_token), and delivers push notifications to the HARP App. The relay stores only the encrypted payload (held in memory until TTL expiry), routing metadata, and request state. It implements the request lifecycle state machine and supports webhook callbacks to the agent platform on state transitions.
HARP App
A mobile application on the user’s device. On receiving a push notification, it fetches the encrypted envelope from the relay, decrypts the payload using the shared key, and displays the full context to the user. The user reviews the action and responds. All approvals are signed with a biometric-gated Ed25519 key stored in the device’s secure enclave. The app stores decision history on-device only.
2.2 Trust Boundaries
Section titled “2.2 Trust Boundaries”The relay operates on a strict separation between routing metadata (which it must see to function) and request content (which it must never see).
Relay sees:
request_id,pair_id,timestamp,ttlexpects_response,push_prioritycallback_url,callback_secret(for webhook delivery)- Request state transitions
Relay never sees:
action,description,parameters,agent_reasoningintent,severity,assurancedecision,reason,form_datacategory,schema,context
Any field that is sensitive is encrypted inside the payload blob before transmission. The relay handles only the outer envelope.
3. Message Format
Section titled “3. Message Format”3.1 Request Envelope (Wire Format)
Section titled “3.1 Request Envelope (Wire Format)”The outer envelope is what the relay receives and stores. It contains routing metadata and the encrypted payload blob. The relay never decrypts payload.
interface RequestEnvelope { version: 1; request_id: string; // UUIDv7 pair_id: string; // UUIDv7, established during pairing timestamp: number; // Unix seconds (UTC) ttl: number; // Seconds until expiry, max 86400 expects_response: boolean; // true for AUTHORIZE/COLLECT, false for INFORM push_priority: "normal" | "high"; callback_url?: string; // Webhook URL for state change notifications callback_secret?: string; // HMAC-SHA256 signing secret for webhook delivery nonce: string; // Base64-encoded, 24 bytes (XChaCha20 nonce) payload: string; // Base64-encoded, XChaCha20-Poly1305 encrypted blob}push_priority is derived from severity: low and medium map to "normal", high and critical map to "high".
3.2 Request Payload (E2E Encrypted)
Section titled “3.2 Request Payload (E2E Encrypted)”The plaintext that the HARP App decrypts and displays. This is encrypted with XChaCha20-Poly1305 using the shared key and the nonce from the outer envelope.
interface RequestPayload { intent: "authorize" | "collect" | "inform"; severity: "low" | "medium" | "high" | "critical"; assurance: "tap" | "biometric" | "elevated"; action: string; description: string; parameters?: Record<string, unknown>; agent_reasoning?: string; context?: Record<string, unknown>; category?: "escalation" | "result" | "status" | "error" | "general"; // INFORM only schema?: CollectSchema; // COLLECT only}3.3 Response Envelope
Section titled “3.3 Response Envelope”The outer envelope the HARP App sends back to the relay after the user makes a decision. Includes a cryptographic signature over the encrypted payload for non-repudiation.
interface ResponseEnvelope { version: 1; request_id: string; // Matches the original request pair_id: string; timestamp: number; // Unix seconds (UTC) nonce: string; // Base64-encoded, 24 bytes (fresh nonce for the response) payload: string; // Base64-encoded, XChaCha20-Poly1305 encrypted blob signature: string; // Base64-encoded, Ed25519 signature (64 bytes) over payload}The signature is computed over the raw payload bytes (before base64 encoding) using the app’s biometric-gated Ed25519 private key. The agent platform verifies this signature against the app’s Ed25519 public key, which was exchanged during pairing.
3.4 Response Payload (E2E Encrypted)
Section titled “3.4 Response Payload (E2E Encrypted)”The plaintext decision or collected data, encrypted by the HARP App before sending.
interface ResponsePayload { intent: "authorize" | "collect" | "inform"; request_id: string; decision?: "approved" | "denied"; // AUTHORIZE only form_data?: Record<string, unknown>; // COLLECT only — field_id → value reason?: string; // Optional user-provided note decided_at: number; // Unix seconds (UTC)}4. Intent Types
Section titled “4. Intent Types”HARP v1 supports three distinct intent types. The intent is set by the agent platform when constructing the request and determines the interaction model on the HARP App.
4.1 AUTHORIZE
Section titled “4.1 AUTHORIZE”Binary approve/deny. Used when an agent needs explicit human sign-off before taking an action.
expects_response:truepush_priority: derived from severity (high/critical→"high")- Response contains
decision: "approved" | "denied" - Agent platform should block execution until a decision arrives or TTL expires
Example use cases: executing a shell command, sending an email, making a purchase, deploying to production, deleting data.
4.2 COLLECT
Section titled “4.2 COLLECT”Structured data collection. Used when an agent needs specific inputs from the user before proceeding.
expects_response:true- Request payload must include a
schemafield (see CollectSchema below) - Response contains
form_data— a map offield_idto typed values - No free-text fields in v1 — all fields have a defined type and constraints
Example use cases: choosing a deployment target, selecting recipients for a report, confirming configuration values, picking a time slot.
CollectSchema
Section titled “CollectSchema”interface CollectSchema { fields: CollectField[];}
interface CollectField { id: string; label: string; type: "select" | "multiselect" | "checkbox" | "number" | "datetime"; required: boolean; options?: string[]; // select, multiselect min?: number; // number max?: number; // number step?: number; // number minDate?: string; // datetime, ISO 8601 maxDate?: string; // datetime, ISO 8601}Schema constraints:
- Maximum 20 fields per schema (
MAX_COLLECT_FIELDS) - Maximum 50 options per
selectormultiselectfield (MAX_SELECT_OPTIONS) - Free-text fields are not supported in v1
Collect Field Types
Section titled “Collect Field Types”| Type | Mobile Component | Value Type | Constraints |
|---|---|---|---|
select | Picker / dropdown | string | Must be one of options |
multiselect | Checkbox group | string[] | Must be subset of options |
checkbox | Toggle switch | boolean | true or false |
number | Numeric input | number | min, max, step |
datetime | Date/time picker | string (ISO 8601) | minDate, maxDate |
4.3 INFORM
Section titled “4.3 INFORM”Fire-and-forget notification. Used when an agent wants to surface information to the user without requiring a decision.
expects_response:false- No
decisionorform_datain the response - Supports an optional
categoryfield to classify the notification type
Categories:
| Category | Description |
|---|---|
escalation | Agent has encountered a situation requiring human awareness |
result | Agent completed a task and is reporting the outcome |
status | Periodic progress update on a long-running task |
error | Agent encountered an error or unexpected condition |
general | General-purpose informational message |
Example use cases: reporting a completed background job, surfacing an anomaly detected during analysis, notifying the user that an agent has paused and is waiting.
5. Severity and Assurance Levels
Section titled “5. Severity and Assurance Levels”5.1 Severity
Section titled “5.1 Severity”Severity describes the impact level of the action being requested. It controls the mobile UX presentation and push notification behavior.
| Severity | Mobile UX | Push Behavior | Default Assurance |
|---|---|---|---|
low | Standard card, muted appearance | Silent / normal priority | tap |
medium | Standard card with accent | Normal priority with badge | biometric |
high | Orange/amber card, vibration | High priority with sound | biometric |
critical | Red full-screen takeover | Critical alert (bypasses Do Not Disturb) | elevated |
Push priority mapping:
low,medium→push_priority: "normal"high,critical→push_priority: "high"
The default assurance column shows recommended assurance values for each severity. The agent platform may specify a higher assurance level than the default for a given severity, but not a lower one.
5.2 Assurance
Section titled “5.2 Assurance”Assurance describes the level of authentication required from the user before a response is accepted.
| Assurance | App Behavior | Recommended Use Case |
|---|---|---|
tap | Tap to acknowledge — no biometric prompt | Low-risk actions, INFORM acknowledgements |
biometric | Face ID / Touch ID required before submitting | Standard approvals and data collection |
elevated | Biometric + explicit confirmation dialog | Destructive actions, production environment changes |
When assurance is elevated, the HARP App presents an additional confirmation step after the biometric check, showing a summary of the action and asking the user to confirm again before signing the response.
6. Pairing Protocol
Section titled “6. Pairing Protocol”Pairing establishes the shared cryptographic material between an agent platform and a user’s HARP App. It happens once per platform/user pair.
Step-by-Step
Section titled “Step-by-Step”- Platform generates an ephemeral X25519 keypair and a unique
pair_id(UUIDv7) - Platform generates a random 32-byte pairing secret
- Platform registers a pairing session on the relay via
POST /v1/pairs/init(relay stores a hash of the secret) - Platform encodes the QR code URI (see below) and displays it to the user
- User scans the QR code with the HARP App
- App generates its own ephemeral X25519 keypair
- App derives the shared secret:
X25519(app_private_key, platform_public_key) - App derives the encryption key via HKDF-SHA256 (see Key Derivation below)
- App generates a persistent Ed25519 signing keypair, gated by the device’s biometric/secure enclave
- App registers its device on the relay via
POST /v1/pairs/register(sends push token) - App sends an encrypted
PairResponseto the relay viaPOST /v1/pairs/:id/complete - Platform polls
GET /v1/pairs/:id/completeto retrieve the encrypted response - Platform derives the same shared secret:
X25519(platform_private_key, app_public_key) - Platform derives the same encryption key and decrypts the
PairResponse - Platform stores the
pair_id, encryption key, and app’s Ed25519 public key locally
QR Code URI Format
Section titled “QR Code URI Format”harp://pair?v=1&pair_id=<uuid_v7>&pub=<base64url_x25519_public_key>&relay=<relay_url>&exp=<unix_timestamp>&secret=<base64url_32_byte_secret>| Parameter | Description |
|---|---|
v | Protocol version (1) |
pair_id | UUIDv7 identifying this pairing session |
pub | Base64url-encoded X25519 public key of the platform |
relay | HTTPS URL of the relay service |
exp | Unix timestamp when the QR code expires (5 minutes from generation) |
secret | Base64url-encoded 32-byte pairing secret (shared over QR, not stored plaintext on relay) |
Key Derivation
Section titled “Key Derivation”shared_secret = X25519(my_private_key, their_public_key)encryption_key = HKDF-SHA256(ikm=shared_secret, salt="harp-v1-enc", info="", length=32)Both parties independently derive the same encryption_key from their own private key and the other party’s public key. The HKDF salt "harp-v1-enc" is a domain separation string ensuring keys derived for HARP cannot be confused with keys derived for other protocols using the same X25519 keypairs.
Pairing Security
Section titled “Pairing Security”- QR code expires after 5 minutes (
PAIRING_EXPIRY = 300) - Ephemeral X25519 keypairs are generated fresh for each pairing — an attacker who intercepts the QR code after expiry cannot derive the shared secret without the platform’s ephemeral private key
- One-time use — the relay invalidates the pairing session after successful completion
- The pairing secret is hashed (SHA-256) before storage on the relay; the relay can verify the secret without storing it in plaintext
7. Cryptographic Primitives
Section titled “7. Cryptographic Primitives”All cryptographic operations use audited, zero-dependency implementations from the @noble/* library family (@noble/curves, @noble/ciphers, @noble/hashes).
| Primitive | Algorithm | Purpose |
|---|---|---|
| Key exchange | X25519 | Derive shared secret from keypairs during pairing |
| Key derivation | HKDF-SHA256 | Derive 32-byte encryption key from shared secret |
| Encryption | XChaCha20-Poly1305 | Authenticated encryption of request/response payloads |
| Signing | Ed25519 | Sign approval responses for non-repudiation |
| Hashing | SHA-256 | Hash pairing secrets for relay verification |
| Webhook signing | HMAC-SHA256 | Sign webhook delivery bodies |
| Padding | Power-of-2 buckets | Prevent payload length analysis (side-channel mitigation) |
Payload padding: Before encryption, payloads are padded to the next power-of-2 bucket (e.g., 128, 256, 512, 1024, 2048 bytes). This prevents an observer at the relay from inferring payload content from ciphertext length.
Nonces: Each request and response uses a fresh random 24-byte nonce. The nonce is included in the outer envelope in plaintext so the recipient can decrypt. Nonces must never be reused with the same key.
8. Request Lifecycle
Section titled “8. Request Lifecycle”8.1 State Machine
Section titled “8.1 State Machine”Every request stored on the relay progresses through a defined set of states. The relay enforces valid transitions and returns 409 Conflict for invalid ones.
States:
| State | Description |
|---|---|
pending | Request received by relay, push notification not yet confirmed |
delivered | Push notification confirmed by mobile push service |
viewed | App fetched the encrypted payload (GET /v1/requests/:id/payload) |
decided | App submitted a response (POST /v1/requests/:id/respond) |
expired | TTL elapsed without a decision |
cancelled | Agent platform cancelled the request before a decision |
Transitions:
pending → delivered Push notification delivery confirmedpending → expired TTL alarm firespending → cancelled DELETE /v1/requests/:id
delivered → viewed App fetches encrypted payloaddelivered → expired TTL alarm fires
viewed → decided App submits responseviewed → expired TTL alarm firesTerminal states: decided, expired, cancelled. Once a request reaches a terminal state, no further transitions are accepted and the state cannot change.
Invalid transition attempts return 409 Conflict with error code INVALID_TRANSITION.
8.2 Idempotency
Section titled “8.2 Idempotency”Submitting a request with the same request_id returns the current status of the existing request rather than creating a duplicate. This allows agent platforms to safely retry POST /v1/requests on network errors.
8.3 Cancellation
Section titled “8.3 Cancellation”The agent platform may cancel a request only while it is in the pending state. Requests in delivered or viewed states cannot be cancelled — the user has already been notified and may be actively reviewing the request. Cancellation is performed via DELETE /v1/requests/:id.
9. Relay API
Section titled “9. Relay API”9.1 Discovery
Section titled “9.1 Discovery”GET /.well-known/harpReturns a JSON document describing the relay’s protocol capabilities, supported versions, and feature flags. Agent platforms should fetch this before their first interaction to confirm version compatibility.
9.2 Pairing Endpoints
Section titled “9.2 Pairing Endpoints”| Method | Path | Description |
|---|---|---|
POST | /v1/pairs/init | Register a new pairing session; returns pair_id |
POST | /v1/pairs/register | App registers its device push token for a pair_id |
POST | /v1/pairs/:id/complete | App submits encrypted PairResponse |
GET | /v1/pairs/:id/complete | Platform polls for PairResponse (long-poll) |
POST | /v1/pairs/:id/device | App updates its device push token (e.g., token rotation) |
DELETE | /v1/pairs/:id | Revoke a pairing from either party |
9.3 Request Endpoints
Section titled “9.3 Request Endpoints”| Method | Path | Description |
|---|---|---|
POST | /v1/requests | Submit an encrypted request envelope |
GET | /v1/requests/:id | Get request status and metadata (no payload) |
GET | /v1/requests/:id/payload | App fetches the encrypted payload; transitions state to viewed |
GET | /v1/requests/:id/response | Platform polls for the encrypted response (long-poll) |
POST | /v1/requests/:id/respond | App submits encrypted response; transitions state to decided |
DELETE | /v1/requests/:id | Platform cancels a pending request |
9.4 Audit Endpoints
Section titled “9.4 Audit Endpoints”| Method | Path | Description |
|---|---|---|
GET | /v1/audit/requests | List requests with filtering and pagination |
GET | /v1/audit/requests/:id | Get full audit record for a specific request |
GET | /v1/audit/reports/summary | Aggregated statistics over a time range |
GET | /v1/audit/export | Export audit records as NDJSON or CSV |
Audit records contain only the envelope-level metadata (request_id, pair_id, timestamps, state transitions). The relay never stores decrypted payload content.
9.5 Webhook Delivery
Section titled “9.5 Webhook Delivery”When a callback_url is provided in the request envelope, the relay sends an HTTP POST to that URL when the request reaches a terminal state (decided, expired, or cancelled).
Request headers:
| Header | Description |
|---|---|
Content-Type | application/json |
X-HARP-Request-Id | The request_id of the completed request |
X-HARP-Timestamp | Unix timestamp of the delivery attempt |
X-HARP-Signature | HMAC-SHA256 hex digest of the request body, keyed with callback_secret |
Request body:
{ "request_id": "...", "status": "decided", "response": { "nonce": "<base64>", "payload": "<base64>", "signature": "<base64>", "timestamp": 1712345679 }}For expired or cancelled status, response is null.
Retry policy: The relay attempts delivery up to 3 times with exponential backoff:
- Attempt 1: immediately
- Attempt 2: 1 second later
- Attempt 3: 5 seconds later
- Final attempt: 25 seconds later
If all attempts fail, the webhook is marked as failed. The platform can still retrieve the response via GET /v1/requests/:id/response.
Signature verification: The platform must verify X-HARP-Signature to confirm the webhook originated from the relay:
expected = HMAC-SHA256(key=callback_secret, message=raw_request_body)valid = constant_time_compare(expected, X-HARP-Signature)9.6 Error Codes
Section titled “9.6 Error Codes”| Code | HTTP Status | Description |
|---|---|---|
INVALID_PAYLOAD | 400 | Request body is malformed or fails validation |
REQUEST_NOT_FOUND | 404 | No request with the given request_id exists |
INVALID_TRANSITION | 409 | The state transition is not allowed from the current state |
PAIR_NOT_FOUND | 404 | No pairing with the given pair_id exists |
REQUEST_EXPIRED | 410 | The request TTL has elapsed |
UNAUTHORIZED | 401 | Missing or invalid authentication credentials |
10. Constants
Section titled “10. Constants”| Constant | Value | Description |
|---|---|---|
PROTOCOL_VERSION | 1 | Current protocol version |
MAX_TTL | 86400 | Maximum TTL in seconds (24 hours) |
DEFAULT_TTL | 300 | Default TTL in seconds (5 minutes) |
PAIRING_EXPIRY | 300 | QR code expiry in seconds (5 minutes) |
NONCE_LENGTH | 24 | XChaCha20-Poly1305 nonce length in bytes |
KEY_LENGTH | 32 | Encryption key length in bytes |
HKDF_SALT | "harp-v1-enc" | HKDF domain separation salt |
MAX_COLLECT_FIELDS | 20 | Maximum number of fields in a COLLECT schema |
MAX_SELECT_OPTIONS | 50 | Maximum options per select or multiselect field |