---
title: Register a tool
description: Make your system something an agent can be authorized for — with its own destination allowlist, risk rating and egress guards.
---

A tool is a registered, callable thing: an endpoint, the capability it requires,
the hosts it may contact, its risk rating, and whether it is dangerous. Agents
can only call tools, and only tools they are explicitly authorized for.

## Register

```bash
curl -X POST https://veefore.example.com/v1/tools \
  -H "Authorization: Bearer $VEEFORE_API_KEY" \
  -H 'content-type: application/json' \
  -H 'Idempotency-Key: register-refund-tool' \
  -d '{
    "workspace_id": "wsp_01JAV…",
    "environment_id": "env_01JAV…",
    "key": "refund_customer",
    "name": "Refund a customer order",
    "description": "Issues a refund against an order in the payments service.",
    "kind": "HTTP",
    "required_capability": "refund.create",
    "endpoint_url": "https://payments.internal.example.com/refunds",
    "http_method": "POST",
    "destination_allowlist": ["payments.internal.example.com"],
    "credential_id": "cred_01JAV…",
    "risk_level": "HIGH",
    "dangerous": true,
    "sensitive_parameters": ["customer.email", "customer.phone"],
    "parameter_schema": {
      "type": "object",
      "required": ["orderId", "reason"],
      "properties": {
        "orderId": { "type": "string" },
        "reason": { "type": "string", "maxLength": 500 }
      }
    }
  }'
```

:::params

:::param workspace_id | string | required
Which workspace owns the tool.
:::

:::param environment_id | string | optional
Scope it to one environment. `null` makes it available across the workspace's
environments. Scope production tools to production.
:::

:::param key | string | required
What the agent passes as `tool`. Stable; policy rules reference it.
:::

:::param kind | enum | required
`HTTP` `CLIENT` `MCP` `A2A` `DATABASE` `WEBHOOK` `SIMULATED`. Only `HTTP`
performs real egress. See [HTTP or CLIENT](#http-or-client).
:::

:::param required_capability | string | required
The capability an agent must hold. Note the field name — it is
`required_capability`, not `capability`.
:::

:::param endpoint_url | string | optional
Required for `HTTP`. Must be HTTPS unless the host is loopback.
:::

:::param http_method | enum | optional
`GET` `POST` `PUT` `PATCH` `DELETE`. Defaults to `POST`.
:::

:::param destination_allowlist | string[] | optional
Hosts this tool may contact. Checked again at the egress boundary.
:::

:::param credential_id | string | optional
The credential the broker leases for this tool. Omit and the tool runs
unauthenticated downstream.
:::

:::param risk_level | enum | optional
`LOW` `MEDIUM` `HIGH` `CRITICAL`. A policy field: `tool.risk_level`.
:::

:::param dangerous | boolean | optional
Marks the tool destructive or high impact. A policy field: `tool.dangerous`.
Templates use it to require approval, and `doctor` flags a dangerous tool with
no rule covering it.
:::

:::param sensitive_parameters | string[] | optional
Dotted paths redacted from logs, audit and the stored result summary.
:::

:::param parameter_schema | object | optional
JSON Schema. Validated before execution, so a malformed call fails at the
gateway rather than downstream.
:::

:::

## HTTP or CLIENT

Both kinds go through the same pipeline: authentication, capability, policy,
risk, approval and audit. They differ in who performs the call once it is
permitted.

| | `HTTP` | `CLIENT` |
| --- | --- | --- |
| Performs the call | The gateway | Your code, inside `guard()` |
| Holds the credential | The broker; the agent never sees it | Your code |
| `endpoint_url`, `credential_id` | Required for real egress | Not allowed |
| Bypassable by skipping the SDK | No, there is nothing to call with | Yes |
| Registered by `veefore init` | No | Yes |

`CLIENT` is how an existing agent gets decisions, approvals and an audit trail
on day one without moving any secrets. `HTTP` is where you end up for tools that
matter: [broker the credential](/build/broker-a-credential), re-register the
tool as `HTTP` with its endpoint, and `guard()` starts returning the gateway's
result instead of running your function. Nothing executes twice, and no code
changes.

```bash
curl -X POST https://veefore.example.com/v1/tools \
  -H "Authorization: Bearer $VEEFORE_API_KEY" \
  -H 'content-type: application/json' \
  -d '{
    "workspace_id": "wsp_01JAV…",
    "key": "lookup_order",
    "name": "Look up an order",
    "kind": "CLIENT",
    "required_capability": "tool.lookup_order",
    "destination_allowlist": ["api.shop.example.com"],
    "risk_level": "LOW"
  }'
```

## Registering grants nothing

```bash
curl -X POST https://veefore.example.com/v1/tools/tool_01ABC/agents \
  -H "Authorization: Bearer $VEEFORE_API_KEY" \
  -H 'content-type: application/json' \
  -d '{
    "agent_id": "agt_01JAV…",
    "parameter_restrictions": { "orderId": { "pattern": "^ord_" } }
  }'
```

:::security Two steps, deliberately
If holding `refund.create` automatically granted access to every tool declaring
it, then registering a new refund tool would silently widen what every existing
finance agent could reach. Tool registration would be a privilege escalation.

With the steps separated, registration grants nobody anything. The two refusals
are distinct so you can tell which half is missing:
`CAPABILITY_NOT_GRANTED` versus `TOOL_NOT_AUTHORIZED`.
:::

`parameter_restrictions` narrows further, per agent. The same tool can be
restricted differently for different agents without a policy rule.

Revoke with `DELETE /v1/tools/{toolId}/agents/{agentId}`.

## Egress guards

When `GATEWAY_ALLOW_REAL_EGRESS=true` and the tool kind is `HTTP`, the gateway
makes a real outbound call. Six guards apply at the egress boundary — after
policy, after approval, after the credential is leased, immediately before bytes
leave the process:

| Guard | Refusal |
| --- | --- |
| Tool kind must be `HTTP` | Otherwise the simulated runtime runs |
| `endpoint_url` must be set | `UNPROCESSABLE` |
| Host must be in the tool's own `destination_allowlist` | `DESTINATION_BLOCKED` |
| HTTPS required unless loopback | `DESTINATION_BLOCKED` |
| `redirect: 'error'` — redirects are never followed | Execution fails |
| Timeout at `GATEWAY_EGRESS_TIMEOUT_MS` (15s) | `EXECUTION_TIMEOUT` |

Plus a circuit breaker per tool: five consecutive failures opens it for 20
seconds, so one broken downstream cannot consume the gateway's capacity.

:::warning Redirects are refused, not followed
A `302` from your downstream fails the execution. This is deliberate: following
a redirect is how an allowlisted host becomes a request to a host that was never
allowlisted, which is server-side request forgery with extra steps.

If your downstream redirects, point `endpoint_url` at the final location.
:::

Headers the gateway always sends:

```http
content-type: application/json
accept: application/json
user-agent: Veefore-Gateway/1.0
x-veefore-request-id: req_01JAV…
x-veefore-agent-id: agt_01JAV…
idempotency-key: …            # when the agent supplied one
```

Plus trace propagation headers, plus whatever the leased credential contributes.
`x-veefore-request-id` is worth logging on your side — it ties your downstream
log line to the Veefore action record.

## Without real egress

With `GATEWAY_ALLOW_REAL_EGRESS=false` — the default — or for kind
`SIMULATED`, the simulated runtime handles execution. It produces deterministic
responses so the full pipeline can be exercised end to end, and **every
response carries `simulated: true`**. Nothing is ever presented as output from a
real system when it is not.

That flag defaults to off so a local run cannot reach a real external system by
accident. Turn it on deliberately. See
[Local development](/build/local-development).

## Disable one

```bash
curl -X POST https://veefore.example.com/v1/tools/tool_01ABC/disable \
  -H "Authorization: Bearer $VEEFORE_API_KEY" \
  -H 'content-type: application/json' \
  -d '{ "reason": "Payments service maintenance window" }'
```

Immediate, for every agent. Refused with `TOOL_DISABLED` — which is distinct
from a policy `BLOCK`, so the agent's logs say what actually happened.

Statuses: `ACTIVE`, `RESTRICTED`, `DISABLED`.

## What policy can see

| Field | Type |
| --- | --- |
| `tool.id` | string |
| `tool.key` | string |
| `tool.kind` | `HTTP` `CLIENT` `MCP` `A2A` `DATABASE` `WEBHOOK` `SIMULATED` |
| `tool.protocol` | `HTTP` `MCP` `A2A` |
| `tool.risk_level` | `LOW` `MEDIUM` `HIGH` `CRITICAL` |
| `tool.dangerous` | boolean |
| `destination.host` | string |
| `destination.risk_level` | `LOW` `MEDIUM` `HIGH` `CRITICAL` |

`tool.dangerous` is the one worth using. Mark destructive tools and let the
starter policy require approval for all of them at once, rather than enumerating
tool keys in a rule that goes stale.

## Practical advice

**Set `dangerous: true` generously.** The cost is an approval step. The cost of
not setting it is an agent doing something irreversible with nobody notified.

**Keep `destination_allowlist` as narrow as the tool needs.** One host is
typical. It is checked twice — in the pipeline and at egress — and the second
check is what protects you if the endpoint is later edited.

**Fill in `sensitive_parameters`.** Redaction is applied on the way in, so a
listed path never reaches logs or audit at all. There is no code path where it
is written and later filtered.

**Use `parameter_schema`.** A malformed call failing at the gateway with
`VALIDATION_FAILED` is much easier to debug than a `500` from your downstream.

## Related

:::cards

:::card Broker a credential | /build/broker-a-credential
Attach a secret the agent never sees.
:::

:::card Capabilities | /concepts/capabilities
The other half of the authorization pair.
:::

:::card Tools API | /reference/api/tools
Every endpoint and permission.
:::

:::card Examples | /build/examples
A real downstream service on loopback, with its own API key.
:::

:::
