Veefore docs

Protect an agent

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

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

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 }),
    ),
  }),
};
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.

#Wrap an agent object

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),
});
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

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

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

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

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

Identity, authority, policy and enforcement for autonomous AI agents.