From b5d91ec06260245ff4a3245cab97e6f3d25bc7eb Mon Sep 17 00:00:00 2001 From: Pavan Dev Singh Charak Date: Sun, 11 Oct 2026 00:52:17 +0530 Subject: [PATCH 1/2] Add Parmana as a TRACE external evidence source --- integrations/parmana-server/README.md | 73 +++++ .../examples/emit_references.py | 59 ++++ ...valid-approval.execution-trust-record.json | 259 ++++++++++++++++++ .../fixtures/02-refusal.refusal-record.json | 42 +++ .../fixtures/03-replay.refusal-record.json | 68 +++++ .../parmana-server/fixtures/approval.json | 26 ++ .../parmana-server/fixtures/approver-key.json | 6 + .../fixtures/parmana-signing-key.json | 11 + .../parmana-server/fixtures/references.json | 36 +++ .../parmana-server/fixtures/report.json | 69 +++++ integrations/parmana-server/integration.yaml | 29 ++ .../parmana-server/parmana_evidence.py | 154 +++++++++++ integrations/parmana-server/pyproject.toml | 17 ++ .../tests/test_parmana_evidence.py | 132 +++++++++ marketplace/catalog.json | 26 +- 15 files changed, 1006 insertions(+), 1 deletion(-) create mode 100644 integrations/parmana-server/README.md create mode 100644 integrations/parmana-server/examples/emit_references.py create mode 100644 integrations/parmana-server/fixtures/01-valid-approval.execution-trust-record.json create mode 100644 integrations/parmana-server/fixtures/02-refusal.refusal-record.json create mode 100644 integrations/parmana-server/fixtures/03-replay.refusal-record.json create mode 100644 integrations/parmana-server/fixtures/approval.json create mode 100644 integrations/parmana-server/fixtures/approver-key.json create mode 100644 integrations/parmana-server/fixtures/parmana-signing-key.json create mode 100644 integrations/parmana-server/fixtures/references.json create mode 100644 integrations/parmana-server/fixtures/report.json create mode 100644 integrations/parmana-server/integration.yaml create mode 100644 integrations/parmana-server/parmana_evidence.py create mode 100644 integrations/parmana-server/pyproject.toml create mode 100644 integrations/parmana-server/tests/test_parmana_evidence.py diff --git a/integrations/parmana-server/README.md b/integrations/parmana-server/README.md new file mode 100644 index 00000000..2e33c03b --- /dev/null +++ b/integrations/parmana-server/README.md @@ -0,0 +1,73 @@ +# Parmana + +[Parmana](https://github.com/pavancharak/parmana) decides whether an agent's action may run under a versioned policy, +requires a signed human approval where the policy says so, and signs a record of every decision, refusals included. + +This integration treats those records as TRACE external evidence. Parmana issues no TRACE Trust Record. An agent +runtime that does points at the Parmana record for its step through `references` (trace-spec §3.1.2): + +| Parmana decision | Parmana record | `references` entries | +| ---------------- | -------------------------- | ---------------------------------------------------------------------- | +| Approved | Execution Trust Record | `authorized-intent` to the record; `approval-outcome` to the approval | +| Refused | Refusal Record | `authorized-intent` to the record | + +One Parmana record per decision. Each entry's `digest` is `sha256:` over the RFC 8785 canonical form of the complete +object as the resolver retains it. The `approval-outcome` entry points at the signed approval inside the Trust Record +(`#/transaction/signals/approvalArtifact`), because Parmana retains it there, under the Trust Record's own signature. + +Parmana signs in its own canonical form, not RFC 8785, and TRACE does not re-sign its records: `parmana_evidence.py` +checks those signatures offline, with Parmana's public key and nothing else. + +## Fixtures + +`fixtures/` is one run of Parmana's refund evaluation, unedited: a refund through `paytm:refund` under policy +`customer-refund` 1.2.0. `fixtures/report.json` names the commit, Node.js version and time of the run. + +| File | What it is | +| ---------------------------------------------- | ------------------------------------------------------------------------------ | +| `01-valid-approval.execution-trust-record.json` | Refund with a manager approval for this order up to 500: approved | +| `02-refusal.refusal-record.json` | Refund with every caller fact `true` and no approval: refused | +| `03-replay.refusal-record.json` | The approval from 01 sent again with a new request: refused, already used | +| `approval.json` | The manager's signed approval used in 01 | +| `parmana-signing-key.json` | Parmana's public key for the run, as `GET /keys/default` served it | +| `approver-key.json` | The approver's public key for the run | +| `report.json` | HTTP status, decision and connector calls per case | +| `references.json` | The `references` entries for each case, written by `examples/emit_references.py` | + +Both keys were generated for the run and do not chain to any trusted issuer. + +## Running it + +``` +pip install -e ".[test]" +python examples/emit_references.py +python -m pytest tests +``` + +`emit_references.py` checks every record's signature, then writes `fixtures/references.json`. The tests check every +signature, that a changed amount, outcome or approval limit no longer verifies, that the approval in 01 is the one the +Trust Record was signed over, that all three decisions name the same policy content hash, and that +`references.json` is exactly what the records produce and validates as `agentrust_trace.Reference`. + +To regenerate the fixtures, run `npm run evaluate:agentrust-refund` in a checkout of Parmana and copy +`evaluations/agentrust-refund/fixtures/` and `evaluations/agentrust-refund/report.json` here, then run +`emit_references.py`. A new run makes new keys and identifiers, so it replaces every file. + +## What this does not claim + +- **No downstream effect.** The connector in the run is a mock, in process. The Trust Record's execution entry records + the mock's response; it is not a confirmed refund. No entry here is, or may be cited as, `observed-effect`, and + `report.json` labels every connector result as a mock. +- **Two caller facts are unchecked.** `refundEligible` and `fraudCheckPassed` are declared by the caller and checked + against no system; under this policy they can only refuse. The signed approval is what authorizes. +- **The policy was not approved through governance in this run.** The run is hermetic, so the Trust Record's + `transaction.policy.governanceAnchor` reads `NO_APPROVAL_RECORD`. +- **A reference is a pointer, not evidence** (trace-spec §3.1.2 rules 3 and 4). A resolved, matching, verifying + Parmana record establishes what Parmana decided and signed, not that the decision was right. +- **No TRACE Trust Record and no conformance level.** The runtime's `subject`, `model`, `data_class` and + `build_provenance` are not Parmana's to state, so this integration fills none of them in. +- **The resolver in `references.json` is an example URL.** A runtime names the Parmana deployment that retains the + record. + +Parmana's server is source available for evaluation; its SDKs, including the `parmana` package these checks use, are +Apache-2.0. Everything in this directory is Apache-2.0. diff --git a/integrations/parmana-server/examples/emit_references.py b/integrations/parmana-server/examples/emit_references.py new file mode 100644 index 00000000..5a2944f9 --- /dev/null +++ b/integrations/parmana-server/examples/emit_references.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Write the TRACE `references` entries for the three fixture decisions. + +For each Parmana record in fixtures/, checks its signature offline, then +writes the entries an agent runtime would sign into its own Trust Record +for that step to fixtures/references.json. It issues no Trust Record: the +runtime's subject, model, data class and build provenance are not Parmana's +to state, so nothing here fills them in. + +The resolver is an example deployment URL. A real runtime names the Parmana +deployment that retains the record. +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) +import parmana_evidence as pe # noqa: E402 + +FIXTURES = ROOT / "fixtures" +RESOLVER = "https://parmana.example.org" +RETENTION = "P1Y" +CASES = { + "valid-approval": "01-valid-approval.execution-trust-record.json", + "refusal": "02-refusal.refusal-record.json", + "replay": "03-replay.refusal-record.json", +} + + +def load(name: str) -> dict: + return json.loads((FIXTURES / name).read_text(encoding="utf-8")) + + +def main() -> int: + signing = load("parmana-signing-key.json") + keys = {signing["keyId"]: signing["pem"]} + out = {} + for case, name in CASES.items(): + record = load(name) + check = ( + pe.verify_trust_record(record, keys) + if "trustRecordId" in record + else pe.verify_refusal_record(record, keys) + ) + if not check.valid: + sys.exit(f"{name} does not verify: {check.errors}") + out[case] = pe.references_for(record, RESOLVER, RETENTION) + for entry in out[case]: + print(f"{case:15} {entry['rel']:18} {entry['digest']}") + (FIXTURES / "references.json").write_text(json.dumps(out, indent=2) + "\n", encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/integrations/parmana-server/fixtures/01-valid-approval.execution-trust-record.json b/integrations/parmana-server/fixtures/01-valid-approval.execution-trust-record.json new file mode 100644 index 00000000..64850906 --- /dev/null +++ b/integrations/parmana-server/fixtures/01-valid-approval.execution-trust-record.json @@ -0,0 +1,259 @@ +{ + "trustRecordId": "6a45f4c3-fe7a-4a54-aab7-369d84a02149", + "businessTransactionId": "de036280-f36d-47df-a309-e4a0093f3b50", + "transaction": { + "businessTransactionId": "de036280-f36d-47df-a309-e4a0093f3b50", + "metadata": { + "businessTransactionId": "de036280-f36d-47df-a309-e4a0093f3b50", + "correlationId": "c2dcc87c-28ec-4e95-a2ba-ef4f61b3198c", + "createdBy": "agentrust-evaluation", + "createdAt": "2026-10-10T19:13:30.554Z" + }, + "authority": { + "authorityId": "65daa3ee-798d-44da-bb5f-beeb1e86bd4c", + "authorityType": "USER", + "principalId": "agentrust-evaluation", + "displayName": "AgenTrust evaluation", + "issuedAt": "2026-10-10T19:13:30.554Z" + }, + "authorization": { + "authorizationId": "5a2f0aa8-ef21-4b51-8500-28821bbe73d6", + "authorityId": "65daa3ee-798d-44da-bb5f-beeb1e86bd4c", + "purpose": "AgenTrust evaluation", + "authorizedAt": "2026-10-10T19:13:30.554Z" + }, + "intent": { + "intentId": "69102182-002f-4c97-a929-237ba88fe4c0", + "authorizationId": "5a2f0aa8-ef21-4b51-8500-28821bbe73d6", + "action": "paytm:refund", + "target": "paytm://orders/order-eval-1", + "parameters": { + "orderId": "order-eval-1", + "transactionId": "txn-order-eval-1", + "amount": 500 + }, + "createdAt": "2026-10-10T19:13:30.554Z" + }, + "policy": { + "name": "customer-refund", + "version": "1.2.0", + "schemaVersion": "1.0.0", + "contentHash": "2a3a9534f0fe15d2d8e1a58b80044a09e78925c1849841af4fc2a6cc7ae78495", + "governanceAnchor": { + "status": "NO_APPROVAL_RECORD" + } + }, + "signals": { + "refundEligible": true, + "managerApproved": true, + "fraudCheckPassed": true, + "refundAmount": 500, + "approvalArtifact": { + "payload": { + "version": 1, + "approvalId": "f356034c-a000-4094-ae52-29f160901ff7", + "issuer": { + "approverId": "manager-eval", + "keyId": "manager-eval-key-1" + }, + "issuedAt": "2026-10-10T19:13:30.553Z", + "expiresAt": "2026-10-10T19:28:30.553Z", + "capability": "paytm:refund", + "resourceId": "order-eval-1", + "scope": { + "field": "value", + "comparator": "lte", + "value": 500 + }, + "nonce": "ebebaaf9-0a0c-42d1-837b-1559f27f32c3" + }, + "signature": { + "algorithm": "ed25519", + "keyId": "manager-eval-key-1", + "value": "ZqLf0CrNxnQ8ZsYTRpkt1SpK0DirBbNr+gac3PDTkLqe5/vyg8toccOxcsJfLnr/4yHBbkL1eKuryeMpTVpECw==", + "signedAt": "2026-10-10T19:13:30.553Z" + } + } + }, + "status": "RECEIVED", + "createdAt": "2026-10-10T19:13:30.574Z" + }, + "authorization": { + "payload": { + "version": 1, + "authorizationId": "4a929904-74a7-42ce-bc1e-76d6d004e84f", + "nonce": "4aa3e1f8-a243-4c42-ac2e-862fea99d4d3", + "decisionId": "81652ae9-7325-48b4-ba33-55e06fbaf90c", + "businessTransactionId": "de036280-f36d-47df-a309-e4a0093f3b50", + "policyName": "customer-refund", + "policyVersion": "1.2.0", + "policyContentHash": "2a3a9534f0fe15d2d8e1a58b80044a09e78925c1849841af4fc2a6cc7ae78495", + "signalsHash": "d00bbbba10aed8ac64e4d6bb84496cf0ce46369db162c3ea1a3b75ffc91bb73c", + "authorizedAt": "2026-10-10T19:13:30.582Z", + "expiresAt": "2026-10-10T19:15:30.582Z", + "businessTransactionHash": "0f6a7ea5fde61612b2301bac3d862cd68bf6d5ee680c2db2a3be77c325aef0a0" + }, + "signature": "y4zx5WQPpx2gFm79UKP0lalv4nsnIkRrgEj7n+MqYT7OBkk3GcNkChrM66mTza/XLXcKKSNfZtcs+NTienzZAw==", + "keyId": "default", + "algorithm": "ed25519" + }, + "overrides": [], + "executions": [ + { + "executionId": "09e2c3d1-8cc0-44c1-a1f2-17c00114120c", + "businessTransactionId": "de036280-f36d-47df-a309-e4a0093f3b50", + "decision": { + "decisionId": "81652ae9-7325-48b4-ba33-55e06fbaf90c", + "intentId": "69102182-002f-4c97-a929-237ba88fe4c0", + "policy": { + "name": "customer-refund", + "version": "1.2.0", + "schemaVersion": "1.0.0" + }, + "signals": { + "refundEligible": true, + "managerApproved": true, + "fraudCheckPassed": true, + "refundAmount": 500, + "approvalArtifact": { + "payload": { + "version": 1, + "approvalId": "f356034c-a000-4094-ae52-29f160901ff7", + "issuer": { + "approverId": "manager-eval", + "keyId": "manager-eval-key-1" + }, + "issuedAt": "2026-10-10T19:13:30.553Z", + "expiresAt": "2026-10-10T19:28:30.553Z", + "capability": "paytm:refund", + "resourceId": "order-eval-1", + "scope": { + "field": "value", + "comparator": "lte", + "value": 500 + }, + "nonce": "ebebaaf9-0a0c-42d1-837b-1559f27f32c3" + }, + "signature": { + "algorithm": "ed25519", + "keyId": "manager-eval-key-1", + "value": "ZqLf0CrNxnQ8ZsYTRpkt1SpK0DirBbNr+gac3PDTkLqe5/vyg8toccOxcsJfLnr/4yHBbkL1eKuryeMpTVpECw==", + "signedAt": "2026-10-10T19:13:30.553Z" + } + } + }, + "outcome": "APPROVED", + "reason": "Refund authorized with a verified manager approval for this order and amount.", + "matchedRuleId": "approve-refund-with-manager-approval", + "evaluatedRules": 5, + "matchedPath": [ + "reject-fraud-check", + "reject-not-eligible", + "reject-invalid-amount", + "reject-above-maximum", + "approve-refund-with-manager-approval" + ], + "evaluatedAt": "2026-10-10T19:13:30.581Z" + }, + "status": "COMPLETED", + "mode": "SYNC", + "startedAt": "2026-10-10T19:13:30.583Z", + "metadata": { + "authorizationId": "4a929904-74a7-42ce-bc1e-76d6d004e84f" + }, + "previousChainHash": null, + "chainHash": "c483393494a8b5265dc753bb36494d182c7bbaba3330db63190296a0739f0f1e", + "chainSignature": { + "algorithm": "ed25519", + "keyId": "default", + "value": "WqsMBBgcvlrKo9Y+r56jkWf5qDGUcoBHxK0IGlZERp/g+y8nPRZ38AwACDrKcxppagNI2D58feGrxVRzUhJEAg==", + "signedAt": "2026-10-10T19:13:30.615Z" + }, + "evidence": { + "businessTransactionId": "de036280-f36d-47df-a309-e4a0093f3b50", + "action": "paytm:refund", + "target": "paytm://orders/order-eval-1", + "parameters": { + "orderId": "order-eval-1", + "transactionId": "txn-order-eval-1", + "amount": 500 + }, + "success": true, + "executedAt": "2026-10-10T19:13:30.613Z", + "attributes": { + "connector": { + "connectorId": "paytm", + "connectorVersion": "1.0.0", + "capability": "paytm:refund", + "sanitizedEndpoint": "paytm://orders/order-eval-1", + "credentialProviderId": "static", + "requestSummary": { + "businessTransactionId": "de036280-f36d-47df-a309-e4a0093f3b50", + "action": "paytm:refund", + "target": "paytm://orders/order-eval-1", + "parameters": { + "orderId": "order-eval-1", + "transactionId": "txn-order-eval-1", + "amount": 500 + } + }, + "responseSummary": { + "success": true, + "metadata": { + "refId": "refid_fa20c63cca33e05a50db4f56", + "orderId": "order-eval-1", + "txnId": "txn-order-eval-1", + "executedAt": "2026-10-10T19:13:30.605Z", + "provider": "paytm", + "resultStatus": "S", + "resultCode": "00", + "sharedSecretRedacted": "[REDACTED]" + } + }, + "startedAt": "2026-10-10T19:13:30.588Z", + "completedAt": "2026-10-10T19:13:30.613Z", + "vendorConfirmationVerified": false, + "connectorEvidenceHash": "498db693ebcfe64b0cc04050421ffce129c585a4c93582a53ef409eb9ad014a7" + } + } + }, + "completedAt": "2026-10-10T19:13:30.614Z" + } + ], + "verifications": [ + { + "verificationId": "e59fdda9-77bc-44a9-964a-06e0ef9d75d3", + "businessTransactionId": "de036280-f36d-47df-a309-e4a0093f3b50", + "status": "VERIFIED", + "message": "Execution Trust Record verified successfully.", + "verifiedAt": "2026-10-10T19:13:30.618Z", + "trustRecordHash": "9f35c8efa1ec6c325cca9b07075ed4c7abef2ddec030c33bbc4d72971a6eb5b4" + } + ], + "receipts": [ + { + "receiptId": "c603b2a7-063c-43c8-9060-0348692f90cc", + "businessTransactionId": "de036280-f36d-47df-a309-e4a0093f3b50", + "trustRecordHash": "9f35c8efa1ec6c325cca9b07075ed4c7abef2ddec030c33bbc4d72971a6eb5b4", + "receiptHash": "0b3b4cdbbce81be58b0570efbe9f8501aaf4dcbeb2d868757eeb913ea83ddad2", + "issuedAt": "2026-10-10T19:13:30.618Z", + "algorithm": "ed25519", + "signature": "GE16mol7Gb7zpqLRQqYZF31OKoFfmvctJogZGRnODb6ZJ0UmdkBDMt+kWzyZOM9hkQ4ubZR+YUwzpLVcXJFoDQ==" + } + ], + "evidenceAnchor": { + "policyContentHash": "2a3a9534f0fe15d2d8e1a58b80044a09e78925c1849841af4fc2a6cc7ae78495", + "governanceAnchorStatus": "NO_APPROVAL_RECORD", + "connectorEvidenceHash": "498db693ebcfe64b0cc04050421ffce129c585a4c93582a53ef409eb9ad014a7", + "anchorHash": "33267148141f96c8c6ad5f6bcb1690feaee38df67068271221f78629237e7bc6" + }, + "createdAt": "2026-10-10T19:13:30.615Z", + "updatedAt": "2026-10-10T19:13:30.615Z", + "trustRecordHash": "9f35c8efa1ec6c325cca9b07075ed4c7abef2ddec030c33bbc4d72971a6eb5b4", + "signature": { + "algorithm": "ed25519", + "keyId": "default", + "value": "m8AfZ7yqdhXbwcg9rPzr8yuSZqn9POXHcISq8dksoEt/GJac006fliHlnuYHXXapAvXdgZfnFBazi/Zel2YNBA==", + "signedAt": "2026-10-10T19:13:30.616Z" + } +} diff --git a/integrations/parmana-server/fixtures/02-refusal.refusal-record.json b/integrations/parmana-server/fixtures/02-refusal.refusal-record.json new file mode 100644 index 00000000..9b400a7c --- /dev/null +++ b/integrations/parmana-server/fixtures/02-refusal.refusal-record.json @@ -0,0 +1,42 @@ +{ + "refusalRecordId": "ba0ae497-3002-4fed-9e6a-38bf915258f8", + "businessTransactionId": "459a463f-58a7-41e6-bb2a-264069f005ca", + "decision": { + "decisionId": "699e7f3e-fc06-46a2-8a0b-3879ea3dba12", + "intentId": "091e547f-280b-4dbd-b228-4c9b185db266", + "policy": { + "name": "customer-refund", + "version": "1.2.0", + "schemaVersion": "1.0.0" + }, + "signals": { + "refundEligible": true, + "managerApproved": true, + "fraudCheckPassed": true, + "refundAmount": 500 + }, + "outcome": "REJECTED", + "reason": "Rejected: declared signal(s) do not match independently verified state (managerApproved=true != verified managerApproved=false).", + "matchedRuleId": "signal-state-verification-violation", + "evaluatedRules": 0, + "matchedPath": [], + "evaluatedAt": "2026-10-10T19:13:30.633Z" + }, + "evaluatedIntent": { + "target": "paytm://orders/order-eval-2", + "parameters": { + "orderId": "order-eval-2", + "transactionId": "txn-order-eval-2", + "amount": 500 + } + }, + "policyContentHash": "2a3a9534f0fe15d2d8e1a58b80044a09e78925c1849841af4fc2a6cc7ae78495", + "createdAt": "2026-10-10T19:13:30.633Z", + "refusalRecordHash": "6d0b41c9c5c3ac30f15d898d799e28a1d6ffa60c671db0e6047ecba36d6c4657", + "signature": { + "algorithm": "ed25519", + "keyId": "default", + "value": "97ezaNZZiavn/qBombUZZjR3+UCCzUvvgt+lDGsH1MXx5Y+B5Ao9+FYS1t+rtvRbKPg9NKvpFmSn7Gnkggl1Dg==", + "signedAt": "2026-10-10T19:13:30.634Z" + } +} diff --git a/integrations/parmana-server/fixtures/03-replay.refusal-record.json b/integrations/parmana-server/fixtures/03-replay.refusal-record.json new file mode 100644 index 00000000..a77b49ec --- /dev/null +++ b/integrations/parmana-server/fixtures/03-replay.refusal-record.json @@ -0,0 +1,68 @@ +{ + "refusalRecordId": "adecc3fc-268a-4520-9a90-d5f01c2acd24", + "businessTransactionId": "cd0e2262-0389-4125-8d97-fd3a67d1f3be", + "decision": { + "decisionId": "2164c336-403e-4149-861e-d7413983f1cd", + "intentId": "34c30cc0-932f-454b-ad6c-d39aef3ea6bd", + "policy": { + "name": "customer-refund", + "version": "1.2.0", + "schemaVersion": "1.0.0" + }, + "signals": { + "refundEligible": true, + "managerApproved": true, + "fraudCheckPassed": true, + "refundAmount": 500, + "approvalArtifact": { + "payload": { + "version": 1, + "approvalId": "f356034c-a000-4094-ae52-29f160901ff7", + "issuer": { + "approverId": "manager-eval", + "keyId": "manager-eval-key-1" + }, + "issuedAt": "2026-10-10T19:13:30.553Z", + "expiresAt": "2026-10-10T19:28:30.553Z", + "capability": "paytm:refund", + "resourceId": "order-eval-1", + "scope": { + "field": "value", + "comparator": "lte", + "value": 500 + }, + "nonce": "ebebaaf9-0a0c-42d1-837b-1559f27f32c3" + }, + "signature": { + "algorithm": "ed25519", + "keyId": "manager-eval-key-1", + "value": "ZqLf0CrNxnQ8ZsYTRpkt1SpK0DirBbNr+gac3PDTkLqe5/vyg8toccOxcsJfLnr/4yHBbkL1eKuryeMpTVpECw==", + "signedAt": "2026-10-10T19:13:30.553Z" + } + } + }, + "outcome": "REJECTED", + "reason": "Rejected: declared signal(s) do not match independently verified state (managerApproved=true != verified managerApproved=false).", + "matchedRuleId": "signal-state-verification-violation", + "evaluatedRules": 0, + "matchedPath": [], + "evaluatedAt": "2026-10-10T19:13:30.649Z" + }, + "evaluatedIntent": { + "target": "paytm://orders/order-eval-1", + "parameters": { + "orderId": "order-eval-1", + "transactionId": "txn-order-eval-1", + "amount": 500 + } + }, + "policyContentHash": "2a3a9534f0fe15d2d8e1a58b80044a09e78925c1849841af4fc2a6cc7ae78495", + "createdAt": "2026-10-10T19:13:30.649Z", + "refusalRecordHash": "876dcb2f44e76d77664dcb823b7aa6ca80d542eaee4ac6afd020edfa59fa46c6", + "signature": { + "algorithm": "ed25519", + "keyId": "default", + "value": "eUUqBBTajfGBiN8GACL/O05mxdJOmKdaPQWUSmzxa3rzh7VYYcf8tba5xTDFFQSVx2C0cyKLkIX+aGVwgZWoCQ==", + "signedAt": "2026-10-10T19:13:30.649Z" + } +} diff --git a/integrations/parmana-server/fixtures/approval.json b/integrations/parmana-server/fixtures/approval.json new file mode 100644 index 00000000..8e375723 --- /dev/null +++ b/integrations/parmana-server/fixtures/approval.json @@ -0,0 +1,26 @@ +{ + "payload": { + "version": 1, + "approvalId": "f356034c-a000-4094-ae52-29f160901ff7", + "issuer": { + "approverId": "manager-eval", + "keyId": "manager-eval-key-1" + }, + "issuedAt": "2026-10-10T19:13:30.553Z", + "expiresAt": "2026-10-10T19:28:30.553Z", + "capability": "paytm:refund", + "resourceId": "order-eval-1", + "scope": { + "field": "value", + "comparator": "lte", + "value": 500 + }, + "nonce": "ebebaaf9-0a0c-42d1-837b-1559f27f32c3" + }, + "signature": { + "algorithm": "ed25519", + "keyId": "manager-eval-key-1", + "value": "ZqLf0CrNxnQ8ZsYTRpkt1SpK0DirBbNr+gac3PDTkLqe5/vyg8toccOxcsJfLnr/4yHBbkL1eKuryeMpTVpECw==", + "signedAt": "2026-10-10T19:13:30.553Z" + } +} diff --git a/integrations/parmana-server/fixtures/approver-key.json b/integrations/parmana-server/fixtures/approver-key.json new file mode 100644 index 00000000..1490aacd --- /dev/null +++ b/integrations/parmana-server/fixtures/approver-key.json @@ -0,0 +1,6 @@ +{ + "approverId": "manager-eval", + "keyId": "manager-eval-key-1", + "algorithm": "ed25519", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA2TnAU0l0I5rfRgnJ5UpbQc4TbQrzPjktkxNMT2JPOnw=\n-----END PUBLIC KEY-----\n" +} diff --git a/integrations/parmana-server/fixtures/parmana-signing-key.json b/integrations/parmana-server/fixtures/parmana-signing-key.json new file mode 100644 index 00000000..32922b82 --- /dev/null +++ b/integrations/parmana-server/fixtures/parmana-signing-key.json @@ -0,0 +1,11 @@ +{ + "keyId": "default", + "algorithm": "ed25519", + "use": "sig", + "pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAl/xcBKrinIKrS+L7gEXvQom5Av+1CMr3BcpykNgqJjE=\n-----END PUBLIC KEY-----\n", + "jwk": { + "crv": "Ed25519", + "x": "l_xcBKrinIKrS-L7gEXvQom5Av-1CMr3BcpykNgqJjE", + "kty": "OKP" + } +} diff --git a/integrations/parmana-server/fixtures/references.json b/integrations/parmana-server/fixtures/references.json new file mode 100644 index 00000000..382f5a2f --- /dev/null +++ b/integrations/parmana-server/fixtures/references.json @@ -0,0 +1,36 @@ +{ + "valid-approval": [ + { + "rel": "authorized-intent", + "id": "trust-records/de036280-f36d-47df-a309-e4a0093f3b50", + "resolver": "https://parmana.example.org", + "digest": "sha256:015e99205fc190b1d27af08c8e89b26bac95ba5190c6502136f8367ec56d5442", + "retention": "P1Y" + }, + { + "rel": "approval-outcome", + "id": "trust-records/de036280-f36d-47df-a309-e4a0093f3b50#/transaction/signals/approvalArtifact", + "resolver": "https://parmana.example.org", + "digest": "sha256:2f33f0354349f9b44cb0215861b996cb86bf9e68d2acb90ec1612ead525bb781", + "retention": "P1Y" + } + ], + "refusal": [ + { + "rel": "authorized-intent", + "id": "refusal/459a463f-58a7-41e6-bb2a-264069f005ca", + "resolver": "https://parmana.example.org", + "digest": "sha256:0e982bdfbcd6dcf3ded382c7ba3b179b44f0a40cdda146dde48db70b21eb3906", + "retention": "P1Y" + } + ], + "replay": [ + { + "rel": "authorized-intent", + "id": "refusal/cd0e2262-0389-4125-8d97-fd3a67d1f3be", + "resolver": "https://parmana.example.org", + "digest": "sha256:95a0252ab33f75c8c0bf1fd7b560aef03c988dd6b7df67ea26b395e1fc316355", + "retention": "P1Y" + } + ] +} diff --git a/integrations/parmana-server/fixtures/report.json b/integrations/parmana-server/fixtures/report.json new file mode 100644 index 00000000..9114dc8e --- /dev/null +++ b/integrations/parmana-server/fixtures/report.json @@ -0,0 +1,69 @@ +{ + "commit": "ac22a7ed594b4a527504ce43841595c1a62f3046", + "workingTreeClean": true, + "node": "v22.22.0", + "ranAt": "2026-10-10T19:13:30.772Z", + "scenario": "paytm:refund under customer-refund 1.2.0", + "connector": "MockPaytmConnectorServer (hermetic, in process)", + "limitation": "refundEligible and fraudCheckPassed are declared by the caller and not checked against any system; they can only refuse. The signed manager approval is what authorizes.", + "totalConnectorInvocations": 1, + "cases": [ + { + "case": "valid-approval", + "approvalAttached": true, + "httpStatus": 200, + "code": null, + "decision": "APPROVED", + "connectorInvocations": 1, + "connectorResult": "mock connector result (MockPaytmConnectorServer); not a confirmed downstream refund", + "reason": null, + "record": { + "type": "ExecutionTrustRecord", + "id": "6a45f4c3-fe7a-4a54-aab7-369d84a02149", + "hash": "9f35c8efa1ec6c325cca9b07075ed4c7abef2ddec030c33bbc4d72971a6eb5b4", + "signatureAlgorithm": "ed25519", + "policyContentHash": "2a3a9534f0fe15d2d8e1a58b80044a09e78925c1849841af4fc2a6cc7ae78495", + "matchedRuleId": null, + "serverVerified": null + } + }, + { + "case": "refusal", + "approvalAttached": false, + "httpStatus": 403, + "code": "POLICY_DENIED", + "decision": "REFUSED", + "connectorInvocations": 0, + "connectorResult": "connector not invoked", + "reason": "Execution rejected: Rejected: declared signal(s) do not match independently verified state (managerApproved=true != verified managerApproved=false).", + "record": { + "type": "RefusalRecord", + "id": "ba0ae497-3002-4fed-9e6a-38bf915258f8", + "hash": "6d0b41c9c5c3ac30f15d898d799e28a1d6ffa60c671db0e6047ecba36d6c4657", + "signatureAlgorithm": "ed25519", + "policyContentHash": "2a3a9534f0fe15d2d8e1a58b80044a09e78925c1849841af4fc2a6cc7ae78495", + "matchedRuleId": "signal-state-verification-violation", + "serverVerified": true + } + }, + { + "case": "replay", + "approvalAttached": true, + "httpStatus": 403, + "code": "POLICY_DENIED", + "decision": "REFUSED", + "connectorInvocations": 0, + "connectorResult": "connector not invoked", + "reason": "Execution rejected: Rejected: declared signal(s) do not match independently verified state (managerApproved=true != verified managerApproved=false).", + "record": { + "type": "RefusalRecord", + "id": "adecc3fc-268a-4520-9a90-d5f01c2acd24", + "hash": "876dcb2f44e76d77664dcb823b7aa6ca80d542eaee4ac6afd020edfa59fa46c6", + "signatureAlgorithm": "ed25519", + "policyContentHash": "2a3a9534f0fe15d2d8e1a58b80044a09e78925c1849841af4fc2a6cc7ae78495", + "matchedRuleId": "signal-state-verification-violation", + "serverVerified": true + } + } + ] +} diff --git a/integrations/parmana-server/integration.yaml b/integrations/parmana-server/integration.yaml new file mode 100644 index 00000000..2c99ccd2 --- /dev/null +++ b/integrations/parmana-server/integration.yaml @@ -0,0 +1,29 @@ +name: Parmana +vendor: Parmana Systems +integrates_with: + - trace +description: >- + Maps signed Parmana authorization decisions and human approvals to TRACE + authorized-intent and approval-outcome references; checks their signatures offline. +maintainer: + github: pavancharak +repository: https://github.com/pavancharak/parmana +homepage: https://docs.parmanasystems.com +license: Apache-2.0 +tier: community +marketplace: + category: Security & policy + mark: PM + keywords: [authorization, approval, refusal, policy, evidence] +# Role. Parmana signs its own records in its own canonical form and issues no +# TRACE Trust Record. An agent runtime's Trust Record points at them through +# `references`; the runtime's subject, model, data_class and build_provenance +# are not Parmana's to state, so this integration fills none of them in. +# External evidence source, no conformance level (#286). +trace_roles: + - external-evidence-source +tested_against: + agentrust-trace: "0.11.0" + # The fixtures are one run of Parmana's refund evaluation at commit + # ac22a7ed594b4a527504ce43841595c1a62f3046 (fixtures/report.json). The checks use the + # published parmana 1.5.0 SDK (Apache-2.0) for Execution Trust Records. diff --git a/integrations/parmana-server/parmana_evidence.py b/integrations/parmana-server/parmana_evidence.py new file mode 100644 index 00000000..bb2b6db9 --- /dev/null +++ b/integrations/parmana-server/parmana_evidence.py @@ -0,0 +1,154 @@ +"""Check Parmana's signed records offline and map them to TRACE `references`. + +Parmana is an external evidence source (trace-spec §3.1.2). It issues no +TRACE Trust Record. An agent runtime that does issue one points at the +Parmana record for its step: + +- `authorized-intent`: the Parmana record of the authorization decision, + an Execution Trust Record when the action was approved and a Refusal + Record when it was refused. +- `approval-outcome`: the signed human approval the decision used, when + there was one. + +Each entry's `digest` is the SHA-256 of the RFC 8785 canonical form of the +complete referenced object, as `resolver` retains it. Nothing here is ever +referenced as `observed-effect`: the connector in these fixtures is a mock, +and Parmana does not observe the downstream state change. + +Signatures are checked against Parmana's own canonical form, not RFC 8785: +Parmana signs its records itself and TRACE does not re-sign them. Execution +Trust Records are checked with the published `parmana` SDK; Refusal Records +and approvals with the same canonical serializer, which that SDK publishes. +""" + +from __future__ import annotations + +import base64 +import hashlib +from dataclasses import dataclass, field +from typing import Any + +import rfc8785 +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey +from cryptography.hazmat.primitives.serialization import load_pem_public_key +from parmana.crypto.canonical import canonical_serialize +from parmana.crypto.offline_verifier import verify_execution_trust_record_offline + +# Large message commitment (v1) from Parmana's KMS signer: a message over +# 4096 bytes may be signed as this prefix plus its SHA-512 digest. +_RAW_MESSAGE_LIMIT = 4096 +_COMMITMENT_PREFIX = b"PARMANA-ED25519-LARGE-MESSAGE-V1\x00" + +# The fields a Refusal Record's hash and signature cover, in Parmana's +# order (packages/crypto/src/RefusalCrypto.ts). An absent field is left out. +_REFUSAL_FIELDS = ( + "refusalRecordId", + "businessTransactionId", + "decision", + "evaluatedIntent", + "bindingViolations", + "submittedBy", + "policyContentHash", + "createdAt", +) + + +@dataclass(frozen=True) +class Check: + valid: bool + errors: list[str] = field(default_factory=list) + + +def _public_key(pem: str) -> Ed25519PublicKey: + key = load_pem_public_key(pem.encode("utf-8")) + if not isinstance(key, Ed25519PublicKey): + raise ValueError("not an Ed25519 public key") + return key + + +def _verify(pem: str, signature_b64: str, message: bytes) -> bool: + key = _public_key(pem) + signature = base64.b64decode(signature_b64) + try: + key.verify(signature, message) + return True + except InvalidSignature: + if len(message) <= _RAW_MESSAGE_LIMIT: + return False + try: + key.verify(signature, _COMMITMENT_PREFIX + hashlib.sha512(message).digest()) + return True + except InvalidSignature: + return False + + +def verify_trust_record(record: dict[str, Any], keys: dict[str, str]) -> Check: + """An Execution Trust Record, with `keys` mapping keyId to PEM.""" + result = verify_execution_trust_record_offline(record, keys) + return Check(result.valid, list(result.errors)) + + +def verify_refusal_record(record: dict[str, Any], keys: dict[str, str]) -> Check: + """A Refusal Record: its hash, and its signature under `keys`.""" + errors: list[str] = [] + message = canonical_serialize({k: record[k] for k in _REFUSAL_FIELDS if k in record}) + if hashlib.sha256(message).hexdigest() != record.get("refusalRecordHash"): + errors.append("refusalRecordHash does not match the record") + signature = record.get("signature") or {} + if signature.get("algorithm") != "ed25519": + errors.append(f"unsupported algorithm: {signature.get('algorithm')}") + elif signature.get("keyId") not in keys: + errors.append(f"no public key for keyId {signature.get('keyId')!r}") + elif not _verify(keys[signature["keyId"]], signature.get("value", ""), message): + errors.append("signature does not verify") + return Check(not errors, errors) + + +def verify_approval(approval: dict[str, Any], keys: dict[str, str]) -> Check: + """A signed human approval: its signature over the payload.""" + signature = approval.get("signature") or {} + key_id = signature.get("keyId") + if key_id != approval.get("payload", {}).get("issuer", {}).get("keyId"): + return Check(False, ["signature keyId differs from the payload's issuer keyId"]) + if key_id not in keys: + return Check(False, [f"no public key for keyId {key_id!r}"]) + message = canonical_serialize(approval["payload"]) + if not _verify(keys[key_id], signature.get("value", ""), message): + return Check(False, ["signature does not verify"]) + return Check(True) + + +def digest(obj: dict[str, Any]) -> str: + """The `digest` of a reference: SHA-256 over RFC 8785 of the whole object.""" + return "sha256:" + hashlib.sha256(rfc8785.dumps(obj)).hexdigest() + + +def references_for( + record: dict[str, Any], resolver: str, retention: str | None = None +) -> list[dict[str, Any]]: + """The `references` entries a runtime's Trust Record would carry for one + Parmana decision. `resolver` is the Parmana deployment that retains it.""" + + def entry(rel: str, id_: str, obj: dict[str, Any]) -> dict[str, Any]: + e = {"rel": rel, "id": id_, "resolver": resolver, "digest": digest(obj)} + if retention: + e["retention"] = retention + return e + + if "trustRecordId" in record: + refs = [entry("authorized-intent", f"trust-records/{record['businessTransactionId']}", record)] + approval = record["transaction"]["signals"].get("approvalArtifact") + if approval is not None: + # Parmana retains the approval inside the Trust Record it authorized, + # under the record's signature, so it is resolved from there. + refs.append( + entry( + "approval-outcome", + f"trust-records/{record['businessTransactionId']}" + "#/transaction/signals/approvalArtifact", + approval, + ) + ) + return refs + return [entry("authorized-intent", f"refusal/{record['businessTransactionId']}", record)] diff --git a/integrations/parmana-server/pyproject.toml b/integrations/parmana-server/pyproject.toml new file mode 100644 index 00000000..6fdc8653 --- /dev/null +++ b/integrations/parmana-server/pyproject.toml @@ -0,0 +1,17 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "parmana-agentrust-integration" +version = "0.1.0" +description = "Parmana authorization decisions as TRACE authorized-intent and approval-outcome references" +requires-python = ">=3.11" +license = "Apache-2.0" +dependencies = ["parmana==1.5.0", "rfc8785", "cryptography"] + +[project.optional-dependencies] +test = ["pytest", "agentrust-trace==0.11.0"] + +[tool.setuptools] +py-modules = ["parmana_evidence"] diff --git a/integrations/parmana-server/tests/test_parmana_evidence.py b/integrations/parmana-server/tests/test_parmana_evidence.py new file mode 100644 index 00000000..10acf890 --- /dev/null +++ b/integrations/parmana-server/tests/test_parmana_evidence.py @@ -0,0 +1,132 @@ +"""Re-check the Parmana fixtures offline, without running Parmana. + +The fixtures are the records one run of Parmana's refund evaluation wrote +(report.json names the commit). These tests check every signature, that a +changed record no longer verifies, that the approval is the one the Trust +Record was signed over, and that references.json is exactly what the +records produce, with no `observed-effect` entry. +""" + +from __future__ import annotations + +import copy +import json +import sys +from pathlib import Path + +import pytest +from agentrust_trace import Reference + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) +import parmana_evidence as pe # noqa: E402 + +FIXTURES = ROOT / "fixtures" + + +def load(name: str) -> dict: + return json.loads((FIXTURES / name).read_text(encoding="utf-8")) + + +TRUST_RECORD = load("01-valid-approval.execution-trust-record.json") +REFUSALS = { + "refusal": load("02-refusal.refusal-record.json"), + "replay": load("03-replay.refusal-record.json"), +} +APPROVAL = load("approval.json") +REPORT = load("report.json") +REFERENCES = load("references.json") +_signing = load("parmana-signing-key.json") +PARMANA_KEYS = {_signing["keyId"]: _signing["pem"]} +_approver = load("approver-key.json") +APPROVER_KEYS = {_approver["keyId"]: _approver["publicKeyPem"]} + + +def test_trust_record_verifies(): + assert pe.verify_trust_record(TRUST_RECORD, PARMANA_KEYS).valid + + +@pytest.mark.parametrize("case", sorted(REFUSALS)) +def test_refusal_record_verifies(case): + assert pe.verify_refusal_record(REFUSALS[case], PARMANA_KEYS).valid + + +def test_approval_verifies_under_the_approver_key(): + assert pe.verify_approval(APPROVAL, APPROVER_KEYS).valid + + +def test_approval_is_the_one_the_trust_record_was_signed_over(): + assert TRUST_RECORD["transaction"]["signals"]["approvalArtifact"] == APPROVAL + + +def test_changed_amount_fails_the_trust_record(): + changed = copy.deepcopy(TRUST_RECORD) + changed["transaction"]["intent"]["parameters"]["amount"] = 50000 + assert not pe.verify_trust_record(changed, PARMANA_KEYS).valid + + +@pytest.mark.parametrize("case", sorted(REFUSALS)) +def test_changed_outcome_fails_a_refusal_record(case): + changed = copy.deepcopy(REFUSALS[case]) + changed["decision"]["outcome"] = "APPROVED" + assert not pe.verify_refusal_record(changed, PARMANA_KEYS).valid + + +def test_raised_limit_fails_the_approval(): + changed = copy.deepcopy(APPROVAL) + changed["payload"]["scope"]["value"] = 500000 + assert not pe.verify_approval(changed, APPROVER_KEYS).valid + + +def test_another_key_fails_every_record(): + other = {"default": _approver["publicKeyPem"]} + assert not pe.verify_trust_record(TRUST_RECORD, other).valid + for record in REFUSALS.values(): + assert not pe.verify_refusal_record(record, other).valid + + +@pytest.mark.parametrize("case", sorted(REFUSALS)) +def test_refusals_are_refusals(case): + assert REFUSALS[case]["decision"]["outcome"] == "REJECTED" + + +def test_every_decision_is_under_the_same_policy_content(): + content_hash = TRUST_RECORD["transaction"]["policy"]["contentHash"] + assert {r["policyContentHash"] for r in REFUSALS.values()} == {content_hash} + + +def test_references_are_what_the_records_produce(): + resolver = REFERENCES["valid-approval"][0]["resolver"] + retention = REFERENCES["valid-approval"][0].get("retention") + assert REFERENCES == { + "valid-approval": pe.references_for(TRUST_RECORD, resolver, retention), + **{c: pe.references_for(r, resolver, retention) for c, r in REFUSALS.items()}, + } + + +def test_references_are_trace_references(): + for entries in REFERENCES.values(): + for entry in entries: + Reference(**entry) + + +def test_relations(): + assert [e["rel"] for e in REFERENCES["valid-approval"]] == [ + "authorized-intent", + "approval-outcome", + ] + for case in REFUSALS: + assert [e["rel"] for e in REFERENCES[case]] == ["authorized-intent"] + + +def test_no_reference_claims_an_observed_effect(): + rels = {e["rel"] for entries in REFERENCES.values() for e in entries} + assert "observed-effect" not in rels + + +def test_the_connector_result_is_labelled_a_mock(): + cases = {c["case"]: c for c in REPORT["cases"]} + assert "mock" in cases["valid-approval"]["connectorResult"] + assert cases["refusal"]["connectorInvocations"] == 0 + assert cases["replay"]["connectorInvocations"] == 0 + assert REPORT["totalConnectorInvocations"] == 1 diff --git a/marketplace/catalog.json b/marketplace/catalog.json index 3c9ca244..64fbbe68 100644 --- a/marketplace/catalog.json +++ b/marketplace/catalog.json @@ -1,6 +1,6 @@ { "catalog_version": 1, - "count": 45, + "count": 46, "integrations": [ { "name": "Claude Code", @@ -731,6 +731,30 @@ "handoff" ] }, + { + "name": "Parmana", + "package_name": "Parmana", + "vendor": "Parmana Systems", + "description": "Maps signed Parmana authorization decisions and human approvals to TRACE authorized-intent and approval-outcome references; checks their signatures offline.", + "path": "integrations/parmana-server", + "url": "https://github.com/agentrust-io/integrations/tree/main/integrations/parmana-server", + "homepage": "https://docs.parmanasystems.com", + "repository": "https://github.com/pavancharak/parmana", + "tier": "community", + "stack": [ + "TRACE" + ], + "category": "Security & policy", + "mark": "PM", + "featured": null, + "keywords": [ + "authorization", + "approval", + "refusal", + "policy", + "evidence" + ] + }, { "name": "ramen-ai cMCP Adapter", "package_name": "ramen-ai cMCP Adapter", From 59ce64f6efb9609b859771166e9abbd16f6a7ab0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 05:21:53 +0000 Subject: [PATCH 2/2] Regenerate the integration index for Parmana Adds the Parmana row that scripts/generate_integration_index.py produces from integrations/parmana-server/integration.yaml. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019v3V53gvHYxvLqP74uAmgF --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3deeabff..86b32892 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ TRACE only works as a standard if it is genuinely neutral. Integrations are list | [OpenAI Agents SDK](integrations/openai-agents/) | agentrust-io | trace | verified | | [OpenShell TRACE Adapter](integrations/openshell/) | agentrust-io | trace | community | | [OpenTelemetry GenAI](integrations/otel-genai/) | agentrust-io | trace | community | +| [Parmana](integrations/parmana-server/) | Parmana Systems | trace | community | | [Observed-effect references](integrations/probityai-observed-effect/) | probityai | trace | community | | [ramen-ai cMCP Adapter](integrations/ramen-ai-cmcp/) | ramen-ai | cmcp, trace | verified | | [SAGE AgenTrust Bridge](integrations/sage-agenttrust/) | SAGE | cmcp, trace | community |