It will check that what an agent did matches what a person authorised, in the real systems, with a signed receipt. It is not finished.
Coming soon. The intent firewall is not finished. Nothing in this part runs in the product today.
How will the intent firewall work?
- A person confirms an intent, and the intent becomes a completion contract.
- The agent acts, with the tools it already has.
- Provely checks the result in the real systems against what the person authorised.
- A signed receipt carries the verdict and the evidence.
In words
- A person confirms an intent, and it becomes a contract.
- The agent acts.
- Provely reads the real systems and checks the result against the contract.
- A signed receipt carries the verdict.
See the planned API
A person confirms an intent, and the intent becomes a completion contract. The agent calls begin, does the work, and asks for verify. The verdict will rest on evidence from the real systems, and a signed receipt will carry it.
The first tab takes one refund from a plain API call to a compiled contract. The second follows one instruction across four systems to a single verdict.
1. A plain API call
This is the refund that every backend already has. It returns and the code moves on.
In words
- The client posts the refund to the provider.
- The provider answers 200 with a refund object.
- The refund holds the state pending.
- No money has left the account yet.
See the refund code
const refund = await stripe.refunds.create({
charge: chargeId,
amount: 14200,
});
// 200. The object exists. Carry on.
return { ok: true, id: refund.id };
refund = stripe.Refund.create(
charge=charge_id,
amount=14200,
)
# 200. The object exists. Carry on.
return {"ok": True, "id": refund.id}
curl -X POST https://api.stripe.com/v1/refunds \
-u "$STRIPE_SECRET_KEY:" \
-d charge="$CHARGE_ID" -d amount=14200
# 200 {"id":"re_1Qx...","status":"pending"}
The status is pending, and only succeeded moves the money. Nothing above reads the status.
2. An agent calls the same thing
Put the same call behind a tool. Now nobody reads the response at all.
In words
- A person asks the agent to refund the customer.
- The agent calls the refund tool.
- The tool returns 200 and a refund object.
- The agent reports "Done. Refunded $142." and never reads the status.
See the tool code
server.tool("refund_customer", schema, async ({ chargeId }) => {
const refund = await stripe.refunds.create({ charge: chargeId, amount: 14200 });
return { content: [{ type: "text", text: `refund ${refund.id} created` }] };
});
// The agent reads that sentence and answers: "Done. Refunded $142."
@server.tool("refund_customer")
def refund_customer(charge_id: str):
refund = stripe.Refund.create(charge=charge_id, amount=14200)
return f"refund {refund.id} created"
# The agent reads that sentence and answers: "Done. Refunded $142."
# The tool result the agent sees:
{"content":[{"type":"text","text":"refund re_1Qx... created"}]}
# The answer the customer sees:
Done. Refunded $142.
That sentence is evidence level E0. It is the agent repeating its own tool call, and it is never sufficient.
3. The same task, compiled
Pass the prompt and the context. Provely compiles the contract, and you confirm it before it runs. Read intent compilation.
In words
- The prompt and the context go into the compiler.
- The compiler returns one step with its evidence level and its correlation keys.
- It adds a negative postcondition that catches a second refund.
- It marks the part that no contract can check as REQUIRES_REVIEW.
const proposal = await provely.compileIntent({
statement: "Refund order 4471 for $142 and email the customer.",
context: { order_id: "4471", customer: "cus_8k2Rv1LmQ9xT" },
tools: [{ name: "stripe.refunds.create", system: "stripe" }],
});
// Show proposal.review to a person. Confirm names every part it cannot check.
const { contract, confirmationId } = await provely.confirmIntent(proposal.proposalId, {
proposalHash: proposal.proposalHash,
confirmedBy: "alex@example.com",
acknowledge: proposal.unchecked.map((part) => part.id),
});
const op = await provely.begin({
contractId: contract.id,
contractVersion: contract.version,
confirmationId,
});
await agent.run(task, { idempotencyKey: op.idempotencyKey });
const v = await provely.verify(op.operationId, { waitMs: 600_000 });
proposal = provely.compile_intent(
statement="Refund order 4471 for $142 and email the customer.",
context={"order_id": "4471", "customer": "cus_8k2Rv1LmQ9xT"},
tools=[{"name": "stripe.refunds.create", "system": "stripe"}],
)
# Show proposal.review to a person. Confirm names every part it cannot check.
confirmed = provely.confirm_intent(
proposal.proposal_id,
proposal_hash=proposal.proposal_hash,
confirmed_by="alex@example.com",
acknowledge_unchecked=[part.id for part in proposal.unchecked],
)
op = provely.begin(
confirmed.contract_id,
version=confirmed.contract_version,
confirmation_id=confirmed.confirmation_id,
)
agent.run(task, idempotency_key=op.idempotency_key)
verdict = provely.verify(op.id, wait=600.0)
curl -X POST https://api.provely.sh/v1/intents \
-H "Authorization: Bearer $PROVELY_API_KEY" \
-d '{"statement":"Refund order 4471 for $142 and email the customer.",
"context":{"order_id":"4471","customer":"cus_8k2Rv1LmQ9xT"},
"tools":[{"name":"stripe.refunds.create","system":"stripe"}]}'
# A person reads the proposal, then confirms every part it does not check.
curl -X POST https://api.provely.sh/v1/intents/$PROPOSAL/confirm \
-H "Authorization: Bearer $PROVELY_API_KEY" \
-d '{"proposal_hash":"'$HASH'","confirmed_by":"alex@example.com",
"acknowledged_unchecked":["u1"]}'
curl -X POST https://api.provely.sh/v1/operations \
-H "Authorization: Bearer $PROVELY_API_KEY" \
-d '{"contract":{"id":"order.refund.completed","version":"'$VERSION'"},
"intent":{"confirmation_id":"'$CONFIRMATION'"}}'
The samples read the key from PROVELY_API_KEY. Get your API key: sign in at app.provely.sh/account/api-keys, open API keys, and create a key. The dashboard shows the key once. Keep it in an environment variable, never in code.
contract: order.refund.completed
subject: { order_id: "4471" }
steps:
- stripe.refund.succeeded
evidence: readback + refund.updated event
correlate: [refund_id, payment_intent]
amount: 14200 usd
negative:
- no second settled refund for this payment intent
review:
- "the customer" resolves to one address
Multi Action workflow
A person writes one sentence: "Refund order 4471 for $142, restock the item, tell the customer, and close the ticket." That sentence is four side effects in four systems. The agent reports one word at the end. This section follows it the whole way.
Step 1 of 5. Read the instruction as separate promises
A workflow is not one promise. It is one promise for each system that changes, and each system draws the line between an acknowledgement and an outcome somewhere else. The table below is the whole difficulty of the task in one place.
| Step | The call returns | Done means | Evidence |
|---|---|---|---|
stripe.refund.succeeded
Stripe | HTTP 200 with a refund object | the refund reaches the state succeeded | readback by refund id, then the refund.updated event E2 + E3 |
shopify.inventory.adjusted
Shopify | the mutation returns no error | the inventory level of that line item rises by one | readback of the inventory level for the line item E2 |
resend.email.delivered
Resend | the send call is accepted | the mail provider reports the message as delivered | the email.delivered event for the message id E3 |
helpdesk.ticket.closed
the helpdesk | the agent says the ticket is closed | the ticket status is closed | readback of the ticket status E2 |
Every acknowledgement in this table is true the moment the agent finishes. Not one of them proves the outcome beside it.
Step 2 of 5. Name what must never happen
A missing step is the smaller danger. The larger one is a step that happens twice. A retry with no idempotency key refunds the customer again, and every positive check still passes. A workflow contract therefore carries negative postconditions, and they are checked with the same evidence as the positive ones.
- no second settled refund exists for this payment intent
- the inventory of this line item is adjusted once, not twice
Each step correlates on the resource that this run created. That is what stops a pre-existing match from verifying. Take an inventory count that already read the right number, a ticket that somebody closed yesterday, or a refund from last week. None of them satisfies a step, because none of them carries the id of this run.
Step 3 of 5. The contract that comes out
In words
- stripe.refund.succeeded: the evidence is the readback by refund id, then the refund.updated event, at level E2 + E3.
- shopify.inventory.adjusted: the evidence is the readback of the inventory level for the line item, at level E2.
- resend.email.delivered: the evidence is the the email.delivered event for the message id, at level E3.
- helpdesk.ticket.closed: the evidence is the readback of the ticket status, at level E2.
- The negative postcondition states that no second settled refund exists for this payment intent.
- One verdict covers all four steps.
contract: support.refund_and_close
subject: { order: "4471", customer: "cus_8k2Rv1LmQ9xT" }
steps:
- id: stripe.refund.succeeded
correlate_by: refund id
evidence: readback by refund id, then the refund.updated event # E2 + E3
- id: shopify.inventory.adjusted
correlate_by: line item id
evidence: readback of the inventory level for the line item # E2
- id: resend.email.delivered
correlate_by: message id
evidence: the email.delivered event for the message id # E3
- id: helpdesk.ticket.closed
correlate_by: ticket id
evidence: readback of the ticket status # E2
negative:
- no second settled refund exists for this payment intent
- the inventory of this line item is adjusted once, not twice
Step 4 of 5. Open one operation for the workflow
You open one operation for the workflow, let the agent do its work, and ask once. The agent needs no knowledge of the contract, and it holds no verifier credential.
const op = await provely.begin({
contractId: 'support.refund_and_close',
contractVersion: '1.0.0',
input: { order: '4471', customer: 'cus_8k2Rv1LmQ9xT', amount_cents: 14200 },
intent: task.prompt,
});
// Your agent runs. It uses op.idempotencyKey for every side effect.
await agent.run(task, { idempotencyKey: op.idempotencyKey });
const v = await provely.verify(op.operationId, { waitMs: 600_000 });
// v.verdict covers all four steps. v.steps holds one row for each of them, and
// v.verdictDocument.decided_by_step names the step that stopped the workflow.
op = provely.begin(
"support.refund_and_close",
version="1.0.0",
input={"order": "4471", "customer": "cus_8k2Rv1LmQ9xT", "amount_cents": 14200},
intent=task.prompt,
)
# Your agent runs. It uses op.idempotency_key for every side effect.
agent.run(task, idempotency_key=op.idempotency_key)
verdict = provely.verify(op.id, wait=600.0)
# verdict.verdict covers all four steps.
# verdict.decided_by_step names the step that stopped the workflow.
provely begin --contract support.refund_and_close --contract-version 1.0.0 \
--input '{"order":"4471","customer":"cus_8k2Rv1LmQ9xT","amount_cents":14200}'
# the agent works with the key that begin printed, then:
provely verify "$OPERATION_ID" --wait 10m --json
Step 5 of 5. Read the audit
The agent reported "Refunded and closed." Three steps passed. The audit below is what the runtime observed, step by step, and it is the same list that the signed receipt carries.
| Step | What the runtime observed | Result |
|---|---|---|
stripe.refund.succeeded | refund re_3Q… read back at succeeded, then refund.updated for the same id | passed, E2 + E3 |
shopify.inventory.adjusted | the level for line item li_88… rose by one after the operation opened | passed, E2 |
resend.email.delivered | no message id, and no delivery event. The agent states that it sent the mail | not proven, E0 only |
helpdesk.ticket.closed | ticket t_5512 read back at closed | passed, E2 |
| negative: no second refund | one settled refund exists for this payment intent | held |
The workflow is not done. Three of four steps carry evidence, and the fourth carries only the word of the agent. An agent assertion is E0, and E0 is never sufficient, so the verdict is UNVERIFIABLE and it names the step.
Each row states what was promised, what was observed, at which evidence level, and against which resource. An auditor settles the run without the agent transcript, and a machine reads the same list out of the signed receipt.
What a longer workflow changes
Nothing in the shape. A workflow of twenty steps is twenty promises, twenty correlation keys and one verdict. Three properties keep it readable at that size.
- A step that no evidence covers is a defect of the contract, not of the run. The compiler marks it REQUIRES_REVIEW and you see it before the workflow runs, never after.
- A step that is still moving holds the whole workflow at PENDING. The verdict never guesses forward, and PENDING is a normal answer.
- One failing step names itself. The verdict carries the step id and the reason code, so the correction is one step, not the whole task again.
Every step above is one operation against one contract. A workflow contract composes them and returns one verdict. Read what a completion contract contains.
What does the agent do next?
The runtime returns a directive and never performs the correction. The directive is also feedback. Its correction names the step with no evidence and the evidence that the step still needs.
In words
- The runtime verifies and returns a directive.
- The directive names the verdict, the reason code, the permitted action, the correction, the idempotency key and the remaining budget.
- The feedback names the step docs.updated and states that it needs a change to the documentation files in commit $SHA.
- The host passes the feedback into the next agent turn.
- The agent finishes the step it dropped, and the runtime verifies again.
{
"schema_version": "1.0.0",
"id": "fd_01ARZ3NDEKTSV4RRFFQ69G5FB1",
"issuer": "provely-runtime",
"issued_at": "2026-10-17T10:00:00.000Z",
"expires_at": "2026-10-17T10:15:00.000Z",
"nonce": "00000000000000000000000000000000",
"organization": "org_demo",
"host": "host_01ARZ3NDEKTSV4RRFFQ69G5FAV",
"operation_id": "op_01ARZ3NDEKTSV4RRFFQ69G5FAV",
"verdict": "UNVERIFIABLE",
"reason_code": "action_result_missing",
"permitted_action": "retry_step",
"correction": "Run the step docs.updated. It needs a change to the documentation files in the commit.",
"tool": {
"name": "update_docs",
"arguments": {
"commit": "4f9c2e1"
}
},
"contract": {
"id": "repo.docs.updated",
"version": "1.0.0",
"hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"idempotency_key": "op_01ARZ3NDEKTSV4RRFFQ69G5FAV",
"retry_budget_remaining": 2,
"requested_by": "operator@example.com"
}
Every directive carries the idempotency key of the first attempt. An executor that reaches the retry budget stops and asks a person.
What will arrive with the intent firewall?
Compiled contracts, signed off
The task text carried an injected instruction. Nobody ran it.
A false-completion alert
The agent retried without the key. Two refunds exist, so: CONTRADICTED.
The audit trail
Every operation, what it promised, and what proves it.
Human in the Loop
The runtime names the correction. A person decides, and a host runs it.
What does Provely do when an agent is attacked?
The contract decides what the agent may do, and what counts as done. Anything outside it fails.
Does Provely stop a prompt injection?
- Provely contains the damage. An action that no person asked for is refused or flagged, whatever made the agent take it. It does not detect the injection.
- A call outside the contract is refused before the provider hears it.
- An action that reaches the provider with the wrong subject or amount is CONTRADICTED.
- A claim of "done" with no evidence is never VERIFIED.
Can the agent pay twice?
- Provely refuses a second operation for the same intent when the caller states no key.
- Every retry directive carries the idempotency key of the first attempt.
Can the agent leak a credential?
- You give the verifier a separate credential. Provely stores it encrypted, and no surface returns it.
- The agent never receives that credential. Operators read the evidence, and no language model sees it.
The refusals above hold when the agent runs with Provely. The verdicts hold on every surface.