---
title: Protect an agent
description: The complete integration — configuration, handshake, decisions, refusals and approvals — in TypeScript and Python.
---

An agent is protected when every tool call it makes is decided by the gateway
first. There are two ways to route calls there:

- **`guard()`** wraps the function that performs a tool. This is what
  `veefore` adds to your code, and what most agents need.
- **`protect()`** wraps an agent object and gives it `act`, `evaluate`,
  `simulate` and `handshake`, for code that calls Veefore directly.

The fastest route is to let the CLI do it:

```bash
npm install -g veefore
cd your-agent-project
veefore
```

It writes the configuration below, registers the agent and its tools, and adds
`guard()`. [Automatic setup](/build/automatic-setup) covers each step. The rest
of this page is the same integration by hand.

## Configure

`veefore init` writes both files. You can also set them by hand.

**`veefore.config.json`** — not secret. Commit it.

```json
{
  "agentId": "agt_01JAV7YQ4T2K9M3E8H6R5N1XZ0",
  "keyId": "key_8f2c1d4e5a6b7c8d",
  "gatewayUrl": "https://gateway.veefore.example.com",
  "gatewayAudience": "https://gateway.veefore.example.com",
  "controlPlaneUrl": "https://veefore.example.com",
  "organizationId": "org_01JAV…",
  "workspaceId": "wsp_01JAV…",
  "environmentId": "env_01JAV…"
}
```

**`.env.local`** — contains the private key. Never commit it.

```ini
# Veefore MANAGED BEGIN — do not edit between the markers
VEEFORE_GATEWAY_URL=https://gateway.veefore.example.com
VEEFORE_GATEWAY_AUDIENCE=https://gateway.veefore.example.com
VEEFORE_AGENT_ID=agt_01JAV7YQ4T2K9M3E8H6R5N1XZ0
VEEFORE_AGENT_KEY_ID=key_8f2c1d4e5a6b7c8d
VEEFORE_AGENT_PRIVATE_KEY=MC4CAQAwBQYDK2VwBCIEI…
VEEFORE_AGENT_NAME=finance-agent
# Veefore MANAGED END
```

The markers matter: re-running `veefore init` replaces only what is between
them and preserves your own variables.

:::warning The audience must match exactly
The gateway compares `aud` against its configured public URL as a **string**.
`https://gateway.veefore.example.com` and
`https://gateway.veefore.example.com/` are different strings, and the mismatch
presents as a bare `401` with no explanation.

```bash
curl -s https://gateway.veefore.example.com/.well-known/veefore-platform-key
```

Use the `audience` from that response. `veefore init` fetches it rather than
assuming.
:::

| Variable | Required | Meaning |
| --- | --- | --- |
| `VEEFORE_GATEWAY_URL` | yes | Where protected actions are sent |
| `VEEFORE_AGENT_ID` | yes | The identity |
| `VEEFORE_AGENT_PRIVATE_KEY` | yes | Base64 PKCS#8 Ed25519 |
| `VEEFORE_AGENT_KEY_ID` | yes | Which key, for rotation |
| `VEEFORE_GATEWAY_AUDIENCE` | recommended | Defaults to the gateway URL |
| `VEEFORE_AGENT_NAME` | no | Display metadata only |
| `VEEFORE_DECLARED_TOOLS` | no | Comma-separated, reported at handshake |

## Guard each tool

:::tabs

:::tab TypeScript
```ts
import { guard } from '@veefore/sdk';

export const tools = {
  refundCustomer: tool({
    description: 'Refund a customer order',
    parameters: z.object({ orderId: z.string(), amount: z.number() }),
    execute: guard('refund_customer', async ({ orderId, amount }) =>
      payments.refunds.create({ orderId, amount }),
    ),
  }),
};
```
:::

:::tab Python
```python
from veefore import guard

@tool
@guard("refund_customer")
def refund_customer(order_id: str, amount: int) -> str:
    """Refund a customer order."""
    return payments.refund(order_id, amount)
```
:::

:::

The key passed to `guard()` is the registered tool key. Each call is
authenticated, checked against capability, policy, risk and approval, and
recorded before your function runs. A refusal throws, and the agent framework
hands the error to the model like any other tool failure, so the model can tell
the user why.

If the tool is a brokered `HTTP` tool, the gateway performs the call with a
credential your code never holds and `guard()` returns that result without
running your function. See [Register a tool](/build/register-a-tool).

## Wrap an agent object

:::tabs

:::tab TypeScript
```ts
import { protect } from '@veefore/sdk';

const agent = protect(myAgent, {
  name: 'finance-agent',
  declaredTools: ['refund_customer', 'order_lookup'],
  eagerHandshake: true,
  onLog: (entry) => logger[entry.level]({ event: entry.event, ...entry.detail }, entry.message),
});
```
:::

:::tab Python
```python
from veefore import protect

agent = protect(
    my_agent,
    name="finance-agent",
    declared_tools=["refund_customer", "order_lookup"],
    eager_handshake=True,
)
```
:::

:::

Configuration comes from the environment, and anything you pass explicitly wins.
Missing required values raise `VeeforeConfigurationError` immediately, naming
what is absent — not three calls later with a confusing `401`.

`eagerHandshake` opens the session at startup so misconfiguration surfaces then
rather than mid-task. `declaredTools` tells the gateway what you intend to call,
so anything you are not authorized for is reported at handshake.

`onLog` receives structured diagnostics and **never receives secrets**. Route it
into your logger; the SDK does not write to stdout on its own.

## What the server says you can do

```ts
const info = await agent.handshake();

console.log(info.mode);              // 'OBSERVE' | 'SIMULATE' | 'ENFORCE'
console.log(info.capabilities);      // ['refund.create', 'order.read']
console.log(info.authorizedTools);   // [{ key, name, capability, status }]
console.log(info.passportVersion);   // 4
console.log(info.expiresAt);         // session expiry
```

:::security The handshake is advisory, not a cache
It tells you what the server currently thinks, which is useful for failing fast
and for logging. It is not a permission grant and nothing in it is trusted on
the next call — every action re-runs the full pipeline.

Do not implement "the handshake said I could, so skip the check". There is no
check to skip; the gateway is the check.
:::

## Act

Three methods, three different contracts.

| Method | On refusal |
| --- | --- |
| `act()` | Throws a typed error |
| `evaluate()` | Returns the decision as data |
| `simulate()` | Evaluates without executing — `dryRun` |

:::tabs

:::tab TypeScript
```ts
const decision = await agent.act({
  tool: 'refund_customer',
  capability: 'refund.create',
  action: 'create',
  parameters: { orderId: 'ord_8812', reason: 'damaged on arrival' },
  resource: { key: 'ord_8812', type: 'order' },
  amount: 250_00,
  currency: 'INR',
  destination: 'payments.internal.example.com',
  purpose: 'Customer reported damage; agent verified photo evidence.',
  idempotencyKey: 'refund-ord_8812-attempt-1',
});

decision.allowed;      // the only field to branch on
decision.decision;     // ALLOW | BLOCK | REQUIRE_APPROVAL | STEP_UP_AUTH | LIMIT | MASK
decision.result;       // the downstream response, when it executed
decision.reason;       // human-readable
decision.explanation;  // policy, version, rule, authority, risk
decision.actionId;     // look it up later
```
:::

:::tab Python
```python
from veefore import ActionRequest

decision = agent.act(
    ActionRequest(
        tool="refund_customer",
        capability="refund.create",
        action="create",
        parameters={"orderId": "ord_8812", "reason": "damaged on arrival"},
        resource={"key": "ord_8812", "type": "order"},
        amount=250_00,
        currency="INR",
        destination="payments.internal.example.com",
        purpose="Customer reported damage; agent verified photo evidence.",
        idempotency_key="refund-ord_8812-attempt-1",
    )
)

decision.allowed
decision.decision
decision.result
decision.reason
decision.explanation
decision.action_id
```
:::

:::

`purpose` is worth filling in. It appears on the approval card, so an approver
reads *why* the agent wants to do this rather than inferring it from parameters.
An approver without context approves everything, which is worse than having no
approval step.

## Handle refusals

:::tabs

:::tab TypeScript
```ts
import {
  VeeforeApprovalRequiredError,
  VeeforeApprovalDeniedError,
  VeeforeBlockedError,
  VeeforeAuthenticationError,
  VeeforeRateLimitError,
  VeeforeConnectionError,
} from '@veefore/sdk';

try {
  const decision = await agent.act(request);
  return { status: 'done', result: decision.result };
} catch (error) {
  if (error instanceof VeeforeApprovalRequiredError) {
    // Expected. Hand back to a human and resume later.
    return { status: 'awaiting_approval', approvalId: error.approvalId };
  }
  if (error instanceof VeeforeApprovalDeniedError) {
    // Expected. Report the reason verbatim.
    return { status: 'denied', reason: error.message };
  }
  if (error instanceof VeeforeBlockedError) {
    // Expected. Never retry a policy refusal.
    return { status: 'refused', reason: error.message };
  }
  if (error instanceof VeeforeAuthenticationError) {
    // Not expected. Identity is wrong — rotated, revoked, or bad audience.
    throw error;
  }
  if (error instanceof VeeforeRateLimitError || error instanceof VeeforeConnectionError) {
    // Transport. Retry with backoff. The SDK already retried.
    throw error;
  }
  throw error;
}
```
:::

:::tab Python
```python
from veefore import (
    VeeforeApprovalDeniedError,
    VeeforeApprovalRequiredError,
    VeeforeAuthenticationError,
    VeeforeBlockedError,
    VeeforeConnectionError,
    VeeforeRateLimitError,
)

try:
    decision = agent.act(request)
    return {"status": "done", "result": decision.result}
except VeeforeApprovalRequiredError as error:
    return {"status": "awaiting_approval", "approval_id": error.approval_id}
except VeeforeApprovalDeniedError as error:
    return {"status": "denied", "reason": str(error)}
except VeeforeBlockedError as error:
    return {"status": "refused", "reason": str(error)}
except VeeforeAuthenticationError:
    raise
except (VeeforeRateLimitError, VeeforeConnectionError):
    raise
```
:::

:::

The one rule: **never retry a refusal**. A `403` is the server's answer and the
answer will not change on the second call. The SDK encodes this — `maxRetries`
applies to transport errors only, never to decisions.
[Full table](/build/errors-and-retries).

## Approvals

```ts
// Block until a human decides, then proceed. Good for a background worker.
const decision = await agent.veefore.actWithApproval(request, {
  timeoutMs: 15 * 60 * 1000,
});
```

Under the hood this evaluates, waits on the approval honouring `pollAfterMs`,
then resubmits the identical request with the `approvalId`. The gateway
re-validates everything — an approval is bound to one request and cannot be
widened or reused. See [Handle approvals](/build/handle-approvals).

## Does it protect everything my agent does?

No, and it is important to be clear about which parts.

It protects every call that goes through `guard()` or `act()`. It does not
intercept arbitrary code, and it would be dishonest to imply otherwise.
`veefore integrate --check` lists every registered tool that is not guarded,
and exits `1` if any are not, so CI can hold that line.

What makes this robust is not the wrapper, it is that the tool's credential
lives in the broker. With a `CLIENT` tool your code still holds the key, so a
call that skips `guard()` still works. With a brokered `HTTP` tool, a call that
skips Veefore has nothing to call with.

If an agent currently holds a long-lived key for your API, moving it into the
broker is the highest-value change you can make.
[Broker a credential](/build/broker-a-credential).

## Related

:::cards

:::card TypeScript SDK | /build/javascript-sdk
The full client surface.
:::

:::card Python SDK | /build/python-sdk
The same, in Python.
:::

:::card Register a tool | /build/register-a-tool
What `tool: 'refund_customer'` has to refer to.
:::

:::card Errors and retries | /build/errors-and-retries
Every error class and what to do with it.
:::

:::
