Skip to main content
Every decision Parmana makes leaves signed evidence. An auditor, a customer or your own agent can check it with Parmana’s public key alone, with no account and no running server.

The records

The records answer different questions. A Trust Record says “this ran, under this decision”. A Refusal Record says “this was asked and refused”. An Execution Intent says “this was about to run”, which matters when the run’s outcome is unknown.

Check a Trust Record offline

Get Parmana’s public key once (GET /keys/default, or client.publicKey("default")), keep it, and check any record against it.
valid is true only when the record’s hash equals a fresh SHA-256 of its canonical form and the Ed25519 signature verifies. Change any field, however deep, and both fail. verifyExecutionIntentOffline and verify_execution_intent_offline do the same for an Execution Intent. The Python verifier is a separate implementation, not a wrapper; python/tests/test_offline_verifier.py checks it against records signed by the TypeScript signer. Three things to know when you build your own verifier:
  • Canonical form. Keys are sorted; the bytes must be exactly what Parmana signed. Serializing the JSON again through a tool that reorders keys or reformats timestamps breaks verification of an untampered record. Keep the JSON exactly as received.
  • Large records. A record over 4096 bytes of canonical content may be signed by AWS KMS over a fixed 97 byte commitment: the bytes of PARMANA-ED25519-LARGE-MESSAGE-V1, one NUL byte, then the SHA-512 digest of the canonical bytes. Try the raw signature first, and the commitment only for a record over 4096 bytes. The SDK functions do this.
  • Hybrid signatures. A record may also carry ML-DSA-65 signatures in signatures. The TypeScript verifier checks them; the Python verifier checks Ed25519 only.
The CLI scripts/verify-trust-record.ts does the same from a terminal. Verify independently walks through it, with a tampered record failing; Detect tampering shows parameter tampering, a forged signature and a reused nonce, each caught.

Check with the server

verify(businessTransactionId) asks the server to verify a record again and appends the result to the record’s history; getLatestVerification() and get_latest_verification() read the last one. These trust the server. The offline functions do not: prefer them when the question is whether to trust the server.

What a record proves, and what it does not

Chapter 10 lists every limit.

When the outcome is unknown

If the release fails after it may have reached the system (a timeout, a crash, an error from the connector), the agent gets 502 EXECUTION_OUTCOME_UNKNOWN and the Execution Intent stays open. The action may have run. Never send it again as a new transaction. An operator closes it:
  1. List what needs attention: GET /execution-intents/unfinalized (or unfinalizedExecutionIntents()). Each entry has the signed intent and its state.
  2. Decide by state.
  3. Resolve after checking the system with the intent’s businessTransactionId, action and target: POST /execution-intents/{id}/resolve with { "resolution": "NOT_EXECUTED" | "EXECUTED", "note": "what you checked" }. The note is required. resolvedBy comes from your key.
A resolution is a person’s attributed statement in the intent’s unsigned status. It is a trail, not tamper evident evidence. The full procedure, with every answer: Execution Intents.

Audit queries

Refunds refused, with their reasons:
Requests refused before any policy ran:
A Refusal Record is written after the refusal, and a failed write does not change the refusal; each miss is logged as refusal_record_write_failed. So a query can miss a refused request, and the logs are the backstop. Policy approvals: npx tsx scripts/verify-policy-changes-approved.ts --full-scan compares every policy on disk with its latest signed approval record.