Skip to content

Developer Quickstart — HARP

Build your first HARP integration

Create artifacts, hash them, send them to a mobile approver, and enforce signed decisions.

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.

  • 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
# 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 hash
signable = canonical_json(artifact) # sorted keys, no whitespace
artifact["artifactHash"] = sha256_hex(signable)
# 3. Encrypt & send to mobile approver
ciphertext = e2e_encrypt(signable, mobile_public_key)
send_to_gateway(ciphertext, metadata)
# 4. Receive signed Decision from mobile
decision = await_decision(artifact["requestId"])
# 5. Verify & enforce
assert 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()

Ready to go deeper?