Developer Quickstart — HARP
Build your first HARP integration
Create artifacts, hash them, send them to a mobile approver, and enforce signed decisions.
The core loop
Section titled “The core loop”1
Wrap the agent action as an artifact
Set artifactType (plan.review, patch.review, command.review, checkpoint.review). Include the payload, requestId, timestamps, and expiresAt.
2
Canonicalize and hash
Serialize to canonical JSON (sorted keys, no whitespace). Compute SHA-256 over the signable form (artifact minus artifactHash).
3
Encrypt and send to mobile
E2E encrypt the artifact to the mobile approver's public key. Route via the gateway (or direct if local).
4
Receive the signed Decision
The mobile approver reviews and signs a Decision (allow/deny) bound to the artifactHash.
5
Verify and enforce
Verify Ed25519 signature, check artifactHash match, validate expiry, check replay cache. Execute only if all pass.
Implementation checklist
Section titled “Implementation checklist”- ☐ Canonical JSON: UTF-8, sorted keys, no whitespace, no trailing newline
- ☐ artifactHash: SHA-256 over signable form (64 lowercase hex chars)
- ☐ Decision signature: Ed25519, base64url without padding
- ☐ Replay cache: store (requestId, artifactHash) tuples — retain at least until expiry + skew
- ☐ Fail closed: any verification failure → deny execution
- ☐ expiresAt on every artifact and Decision
Pseudocode
Section titled “Pseudocode”# 1. Build artifact (plan, patch, command, checkpoint)artifact = { "requestId": generate_ulid(), "artifactType": "command.review", "repoRef": "repo:acme/widgets", "createdAt": now_utc(), "expiresAt": now_utc() + timedelta(minutes=10), "payload": {"kind": "command", "command": "npm run build"}, "artifactHashAlg": "SHA-256"}
# 2. Canonical hashsignable = canonical_json(artifact) # sorted keys, no whitespaceartifact["artifactHash"] = sha256_hex(signable)
# 3. Encrypt & send to mobile approverciphertext = e2e_encrypt(signable, mobile_public_key)send_to_gateway(ciphertext, metadata)
# 4. Receive signed Decision from mobiledecision = await_decision(artifact["requestId"])
# 5. Verify & enforceassert verify_ed25519(decision, mobile_public_key)assert decision["artifactHash"] == artifact["artifactHash"]assert decision["expiresAt"] > now_utc()assert not replay_cache.contains(decision["nonce"])replay_cache.add(decision["nonce"], decision["expiresAt"])
if decision["decision"] == "allow": execute(artifact)else: deny()