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?

  1. A person confirms an intent, and the intent becomes a completion contract.
  2. The agent acts, with the tools it already has.
  3. Provely checks the result in the real systems against what the person authorised.
  4. A signed receipt carries the verdict and the evidence.
begincontract + inputactthe agent, with the keyaction_resultthe acknowledgement (E1)verifyreadback, event (E2, E3)receiptEd25519 signedthe agent does step 2 · the runtime does steps 4 and 5
Intent, contract, action, check, receipt.
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.

your codePOST /v1/refundsStripe200 OKrefund re_1QxT7dstatus: pendingthe request is acknowledgedthe outcome is not decidedthe bankno money has left the accountfour of the five states end hereA 200 answers the call. It does not answer the question.
The call returns 200. The money has not moved.
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
refund.ts
const refund = await stripe.refunds.create({
  charge: chargeId,
  amount: 14200,
});

// 200. The object exists. Carry on.
return { ok: true, id: refund.id };
refund.py
refund = stripe.Refund.create(
    charge=charge_id,
    amount=14200,
)

# 200. The object exists. Carry on.
return {"ok": True, "id": refund.id}
refund.sh
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.

the promptthe agenttool callStripe200 pendingthe agent reads the status code and stops“Done. The customer has beenrefunded $142.”an agent assertion is evidence level E0E0 is never sufficient on its ownNobody compares the claim with the record, so nobody finds the difference.
The tool call succeeded, so the agent reports success.
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
agent.ts
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."
agent.py
@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."
agent.sh
# 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.

“Refund order 4471 for $142and email the customer.”context, toolsyou pass thiscompilestepstripe.refund.succeededevidenceE2 readback + E3 refund.updatedcorrelaterefund_id, payment_intentnegativeno second settled refundREQUIRES_REVIEW“the customer” resolves to one addresswhat done meanshow the runtime proves ityou confirm this once
The prompt goes in. The postcondition comes out.
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.
verify.ts
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 });
verify.py
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)
verify.sh
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.

What the compiler returned
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
compiled1 stepE2 + E32 correlation keys1 negative postcondition1 REQUIRES_REVIEW

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.

Four steps, four systems, four different meanings of "done".
StepThe call returnsDone meansEvidence
stripe.refund.succeeded StripeHTTP 200 with a refund objectthe refund reaches the state succeededreadback by refund id, then the refund.updated event E2 + E3
shopify.inventory.adjusted Shopifythe mutation returns no errorthe inventory level of that line item rises by onereadback of the inventory level for the line item E2
resend.email.delivered Resendthe send call is acceptedthe mail provider reports the message as deliveredthe email.delivered event for the message id E3
helpdesk.ticket.closed the helpdeskthe agent says the ticket is closedthe ticket status is closedreadback 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
correlationstripe: refund idshopify: line item idresend: message idhelpdesk: ticket id

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

stripe.refund.succeededreadback by refund id, then the refund.updated eventE2 + E3shopify.inventory.adjustedreadback of the inventory level for the line itemE2resend.email.deliveredthe email.delivered event for the message idE3helpdesk.ticket.closedreadback of the ticket statusE2one verdict, four stepsnegative postcondition: no second settled refund exists for this payment intent
Four steps, four systems, one verdict over all of them.
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.
The workflow contract
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
compiled4 steps4 systemsE2E32 negative postconditions

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.

support.ts
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.
support.py
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.
support.sh
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.

The audit of one run. Three steps hold evidence. One holds a sentence.
StepWhat the runtime observedResult
stripe.refund.succeededrefund re_3Q… read back at succeeded, then refund.updated for the same idpassed, E2 + E3
shopify.inventory.adjustedthe level for line item li_88… rose by one after the operation openedpassed, E2
resend.email.deliveredno message id, and no delivery event. The agent states that it sent the mailnot proven, E0 only
helpdesk.ticket.closedticket t_5512 read back at closedpassed, E2
negative: no second refundone settled refund exists for this payment intentheld

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.

  1. 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.
  2. A step that is still moving holds the whole workflow at PENDING. The verdict never guesses forward, and PENDING is a normal answer.
  3. 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.

verifydirectivestep: docs.updatedneeds: a change to the documentation files in commit $SHAthe directive is also feedbackfeedbackthe agentverify againthe loop closes on evidence, not on a retry countevery directive carries the first idempotency key
verify, directive, agent, verify. The loop closes on evidence, not on a retry count.
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.
The directive a host passes into the next turn
{
  "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.