# What will the intent firewall check?

> The intent firewall will check that what an agent did matches what a person authorised, with a signed receipt. It is not finished.

Canonical: https://provely.sh/intent-firewall  
Last updated: 2026-10-18  

> **Coming soon.** Verification is not in the product yet.


**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.

*Figure: Intent, contract, action, check, receipt.*

- 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.

<details>
<summary>See the planned API</summary>

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.

### Single action

### 1. A plain API call

This is the refund that every backend already has. It returns and the code moves on.

*Figure: The call returns 200. The money has not moved.*

- 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.

refund.ts

```typescript
const refund = await stripe.refunds.create({
  charge: chargeId,
  amount: 14200,
});

// 200. The object exists. Carry on.
return { ok: true, id: refund.id };
```

refund.py

```python
refund = stripe.Refund.create(
    charge=charge_id,
    amount=14200,
)

# 200. The object exists. Carry on.
return {"ok": True, "id": refund.id}
```

refund.sh

```bash
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.

*Figure: The tool call succeeded, so the agent reports success.*

- 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.

agent.ts

```typescript
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

```python
@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

```bash
# 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](/docs/intent-compilation).

*Figure: The prompt goes in. The postcondition comes out.*

- 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

```typescript
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

```python
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

```bash
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](https://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*

```yaml
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
```

compiled

`1 step` `E2 + E3` `2 correlation keys` `1 negative postcondition` `1 REQUIRES_REVIEW`

### Multi Action workflow

### 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".*

| 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

correlation

`stripe: refund id` `shopify: line item id` `resend: message id` `helpdesk: 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

*Figure: Four steps, four systems, one verdict over all of them.*

- 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*

```yaml
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
```

compiled

`4 steps` `4 systems` `E2` `E3` `2 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

```typescript
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

```python
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

```bash
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.*

| 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.

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](/contracts).

## 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.

*Figure: verify, directive, agent, verify. The loop closes on evidence, not on a retry count.*

- 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*

```json
{
  "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.

</details>

## 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.

## Read next

- [Read how Provely works today](https://provely.sh/how-it-works)
- [See the measured coverage](https://provely.sh/defensive-coding)
