The problem: agents guess
An agent that guesses is a liability. Guess a policy version and it is refused when a new one is approved. Guess which facts a policy reads and it leaves one out. Guess that a timeout means “failed” and the customer is paid twice. So I tried to remove every guess. The agent asks the server what it needs, sends one object, and reads one answer from a fixed list.The idea: the agent proposes, the server tells it what a proposal must carry
The agent never decides whether an action is allowed. It proposes an intent (“refund ₹500 on this order”), Parmana decides, and the agent acts correctly on the answer.How it works, step by step
The integration page for agents is an ordered procedure, meant to be followed literally. In short:- Check the server.
GET /healthandGET /ready. If/readyreports authentication switched off, the agent stops and sends nothing real. That server accepts requests from anyone. - Check its own key.
GET /callers/mereturns the caller id and the actions the key may use. A refund agent’s key should listpaytm:refundand nothing else. - Ask what the policy needs.
GET /policies/in-effect?capability=paytm:refundreturns the policy name and version in effect, every fact its rules read with the declared type, which facts must equal a value in the request (the refund amount must equal the amount being refunded), and which fact needs a signed human approval, with where in the request the approved resource and amount are. - Build one Business Transaction. A JSON object with a fresh transaction id, the agent’s authority, the purpose, the intent (action, target, parameters), the policy exactly as step 3 returned it, and the signals.
- Send it to
POST /execute, once. - Read the answer and act on the first matching case.
503, never a partial answer. Ask again before every request: an approved new version takes effect at once.
The first answer is usually “no”, and that is correct
Every action needs a signed approval from a trusted person for that action and that resource, reads included. A server refuses to load a policy that could approve without one. So the agent’s first refund request on a new order is refused with403 POLICY_DENIED.
That is the normal first step. A person signs an approval on their own machine, naming the action, the order and the amount. It is usually valid for 15 minutes and works once. The agent sends a new request, with a new transaction id, the approval fact set to true and the signed approval attached. The server does not believe “true” on its own. It checks the signature, the approver, the action, the resource, the amount, the expiry and that the approval has not been used.
What comes back
The agent reads one of a fixed set of answers. The ones that matter most:200with outcomeAPPROVED. The action was released, and the response is a signed Execution Trust Record. Store it with the transaction id. It is the proof.403 POLICY_DENIED. A rule refused, or an approval is missing. Final for that request. Do not tweak the request until it passes.503withSIGNING_UNAVAILABLEorEXECUTION_INTENT_UNAVAILABLE. Refused before release. Nothing ran. Safe to try later with a new transaction id.502 EXECUTION_OUTCOME_UNKNOWNor500 EXECUTION_RECORD_INCOMPLETE. The action was released. It may have happened. Never resend. Tell the operator.- A timeout. Unknown. Look the transaction up by its id first.
POST /execute automatically, and set a long timeout. In production on 2026-10-01 an approved request took 24 to 32 seconds end to end, longer than the SDKs’ default 30 second timeout. The docs now say 120 seconds.
The SDKs
There are two, at the same version:- TypeScript:
npm install @parmana/sdk - Python:
pip install parmana
- A builder (
createBusinessTransaction(),create_business_transaction()) that generates the ids and keeps the linked ones consistent. Most early400errors were hand-written ids that must match. policyInEffect()andpolicy_in_effect()for step 3, since 1.5.0.- Typed errors with the HTTP status and the server’s code, so “nothing ran” and “it may have run” are different branches in code, not string matching.
- Offline verification of a returned record with only the public key.
execute(), deliberately.
A public sandbox with one action that acts on nothing lets you see the refusal, get a demo approval and verify the record before connecting anything real.
What it does not do
- It does not check most facts the agent declares. For a refund, “eligible” and “fraud check passed” are the agent’s word. Under the current refund policy they can only cause a refusal, never an approval: the signed approval is what authorizes. Registering the business system that owns those facts is built, but no real one is connected yet.
- An approval does not fix every parameter. It covers the action and the resource, and the amount where the policy names one. A Slack approval does not fix the message text.
- It does not hold a refused request open. The agent sends a new one.
- A
200needs a connector. On a server with no connector for the action, an approved request gets503 CONNECTOR_NOT_REGISTEREDand nothing runs. - It is not fast. Tens of seconds per approved request in production today, and the cause is not yet measured.
- Only TypeScript and Python. No other SDK exists; anything that speaks HTTP can follow the procedure.
- It governs only what is routed through it. An agent that also holds its own payment credentials can refund without asking anyone.