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.
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 gets502 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:
-
List what needs attention:
GET /execution-intents/unfinalized(orunfinalizedExecutionIntents()). Each entry has the signed intent and its state. -
Decide by state.
-
Resolve after checking the system with the intent’s
businessTransactionId,actionandtarget:POST /execution-intents/{id}/resolvewith{ "resolution": "NOT_EXECUTED" | "EXECUTED", "note": "what you checked" }. The note is required.resolvedBycomes from your key.
Audit queries
Refunds refused, with their reasons: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.