> ## Documentation Index
> Fetch the complete documentation index at: https://www.plungeai.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Tools plane

> A tool agent is any active registry agent with a typed invocation contract. The tools plane is the shortest path from "I want Brave search" to a result: three routes, one security check, one execution path. Every call becomes a one-task workflow on the engine, so it lands in your execution …


A **tool agent** is any active registry agent with a typed invocation contract. The tools plane is the shortest path from "I want Brave search" to a result: three routes, one security check, one execution path. Every call becomes a one-task workflow on the engine, so it lands in your execution history and carries an `x-request-id` you can trace ([Traces](/one-api/traces-observability)).

```
Discover  ──►  GET  /v1/tools                 who is callable (the active catalog)
Inspect   ──►  GET  /v1/tools/:id             the contract: operations, params, examples
Execute   ──►  POST /v1/tools/:id/execute     contract check → trust fence → engine → result
```

Auth: `Authorization: Bearer $PLUNGE_API_KEY` (an `ozk_` key — see [Authentication & keys](/one-api/authentication)).

## 6.1 List the catalog — `GET /v1/tools`

Returns the active registry agents as summary cards. Paging: `limit` (default 50, max 100) and `offset`. The response `count` is the size of the page you received, not the catalog total — page until a short page comes back.

```bash
curl -s "https://api.plungeai.com/v1/tools?limit=100" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"
```

Response shape: `{ "tools": [ { "id", "type": "agent", "name", "description", "category", "tags": [...], "status": "active", "credential": {...}, ... } ], "count" }`. `credential` says what the agent runs on: `{ "tier": "platform" }` (our keys — nothing to set up) or `{ "tier": "connection", "provider", "method": "oauth" | "api_key" | "wallet" }` (the caller's own account at that provider — see [Connectors](/one-api/connectors)). Tags worth reading: `gated:<operation>` marks operations the trust fence will refuse unattended (see 6.4), `status:active` is the only status this route lists.

On 2026-09-18 the catalog had **92 active tool agents in 14 categories** (`GET /v1/agents/categories` gives the live breakdown; the same query backs `GET /v1/agents`, so the two lists are identical — the difference is how you call them).

## 6.2 Inspect the contract — `GET /v1/tools/:id`

```bash
curl -s "https://api.plungeai.com/v1/tools/brave-agent" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"
```

| Field | Meaning |
|---|---|
| `agent_id`, `name`, `description` | the card identity |
| `operations[]` | `{ name, description?, required_params? }` — the verbs the agent accepts as `operation`; an agent with no operations takes a `prompt` (or its `soleRequiredParam`) |
| `inputSchema` | JSON Schema (`type: object`, `properties`, `required`, `additionalProperties`) for `params`; per-operation requireds live on `operations[].required_params` |
| `examples[]` | `{ title, cnl }` — copy-paste CNL task snippets from the card's demonstrations |
| `output_type` | prose description of what comes back |
| `credential` | `{ tier: "platform" }` or `{ tier: "connection", provider, method }` — the same value the list carries; `GET /v1/tools/stripe-agent` answers `{ "tier": "connection", "provider": "stripe", "method": "api_key" }` |
| `soleRequiredParam` | present when exactly one string param is required — a bare `prompt` is mapped onto it |
| `credentials` | filled by surfaces that know the user's connections; absent here |

A connection-tier agent says so in the same card:

```bash
curl -s "https://api.plungeai.com/v1/tools/stripe-agent" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"
```

Unknown id → `404 unknown_tool` (also in the `X-Error-Code` header):

```bash
curl -s -o /dev/null -w '%{http_code} %header{x-error-code}\n' \
  "https://api.plungeai.com/v1/tools/no-such-agent" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"
```

## 6.3 Execute — `POST /v1/tools/:id/execute`

Body (JSON by default; the same fields as YAML with `Content-Type: text/yaml`). The tool's fields may be
sent bare at the root, exactly as the agent's README documents them (`{"expression": "7*6"}`); `params:`
is still accepted and wins on a name clash.

| Field | Type | Notes |
|---|---|---|
| `operation` | string | one of `operations[].name`; omit for prompt-only agents |
| *the tool's fields* | per the contract | bare at the root (`query`, `count`, `expression`, …); validated against the contract |
| `params` | object | the same fields wrapped; merged over the bare ones. Names that collide with task-envelope fields (`agent`, `id`, `type`, …) are `reserved` and rejected either way |
| `prompt` | string | free-text input; satisfies missing requireds on prompt-capable agents |
| `format` | `json` · `yaml` · `markdown` · `text` | response negotiation; `Accept` and the request `Content-Type` also count (default JSON, YAML in → YAML out) |

Order of evaluation, from `orchestration/api-gateway/routes/tools.ts`:

1. **Contract validation.** `reserved` names, or missing requireds on a prompt-less call → `422 invalid_params` with `{ missing, reserved, warnings, contract }` — the contract is echoed so you can fix the call without a second request.
2. **The trust fence** (`checkAgentCall`, mode `unattended`) → `403 refused` or `409 approval_required` — see 6.4. Nothing is executed and no execution row is written.
3. **Execution.** A `workflow_executions` row opens under your user (`execution_id` = the request's trace id), the engine runs the one-task wrapper, the row closes with the result.

Success (`200`):

```json
{ "ok": true, "content": "…markdown or text…", "outcome": "ok", "execution_id": "…", "request_id": "…" }
```

```bash
curl -s -X POST "https://api.plungeai.com/v1/tools/brave-agent/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H "Content-Type: application/json" \
  -d '{"prompt":"cloudflare workers pricing","count":3}'
```

Bare fields, YAML in → YAML out (the Content-Type mirror):

```bash
curl -s -X POST "https://api.plungeai.com/v1/tools/calculator-tool-agent/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary $'expression: "7*6"\n'
```

Operation form (the weather agent takes `location`; here the contract says what is missing):

```bash
curl -s -w '\n%{http_code}\n' -X POST "https://api.plungeai.com/v1/tools/weather-agent/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H "Content-Type: application/json" \
  -d '{"operation":"current","params":{}}'
```

## 6.4 Outcomes and the trust fence

An agent that cannot do the job answers with a structured **outcome**, not prose (`core/core-base/agent-outcome.ts`). The tools plane maps outcomes onto HTTP (`routes/_util.ts` `outcomeStatus`):

| `outcome` | HTTP | `error.code` | What to do |
|---|---|---|---|
| `ok` | 200 | — | read `content` |
| `needs_input` | 422 | `invalid_params` | fix `params` — `error.detail` and the echoed `contract` say how |
| `needs_connection` | 424 | `connection_required` | the account behind the call has not connected this provider — `error.connect` names the way in, see [Connectors](/one-api/connectors) |
| `needs_api_key` | 424 | `credential_required` | the agent needs a key the account has not stored — `error.connect` names the way in |
| `unavailable` | 503 | `agent_unavailable` | upstream/provider down — retry later or pick another agent |
| `error` | 502 | `execution_failed` | the agent failed; `error.message` carries the agent's own text |

The error body is `{ "error": { "code", "message", "outcome", "detail"?, "content"?, "contract"?, "connect"? } }` plus an `X-Error-Code` header. On a `424`, `connect` is `{ "provider", "method", "link": "POST /v1/connections/<provider>/link" }`; the Markdown, YAML and text wires print it as a `connect:` line under the execution footer.

**The trust fence** (`core/core-base/call-agent.ts`) runs before every execution and cannot be turned off from the API:

- **Always-gated operations** — `buy`, `purchase`, `fetch_paid`, `send`, `transfer`, `pay`, `send_payment`, `withdraw`, `shop` — plus any `gated:<op>` tag on the card. On this unattended surface they return `403 refused` (a human-in-the-loop surface such as Studio or a harness mission with an explicit allow-list returns `409 approval_required` instead).
- **Guarded categories** — agents in the `payment` and `blockchain` categories are refused unattended unless a mission's `allowed_agents` names them.
- **Not active** — a card that is not `status:active` is `403 agent_not_active`; an unknown id is `404 unknown_tool`.

```bash
curl -s -w '\n%{http_code}\n' -X POST "https://api.plungeai.com/v1/tools/resend/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H "Content-Type: application/json" \
  -d '{"operation":"send","params":{"from":"hello@example.com","to":"nobody@example.com","subject":"x","text":"x"}}'
```

A refusal writes no execution row — a denial is not a run.

## 6.5 The catalog by category (live 2026-09-18)

Ids are exact; `GET /v1/tools/<id>` is the source of truth for operations and for `credential`. **Credential** is that field: `platform` runs on our keys; `connection` needs the caller's own account at the named provider, connected once over the API (chapter 7). A card whose agent also needs an account that is not yet in the machine-readable table (`core/core-base/credential-tiers.ts`) reads `platform`; those are named in the row.

| Category | Agents | Credential |
|---|---|---|
| **search** (14) | `brave-agent`, `exa-agent`, `firecrawl-agent`, `tavily-agent`, `serper-agent`, `serpapi-agent`, `crawl4ai-agent`, `browserbase`, `arxiv-search`, `wikipedia-search`, `newsapi-get-articles`, `nyt-api`, `product-hunt-agent`, `branding-agent` | `platform` |
| **tools** (23) | `calculator-tool-agent`, `markitdown`, `weather-agent`, `wolfram-alpha-full-results`, `data-analyst`, `code-reviewer`, `technical-writer`, `research-analyst`, `research-team`, `learn`, `pdf-worker`, `pptx-worker`, `xlsx-worker`, `ms-office-worker`, `report-worker`, `render-worker`, `cloud-browser-agent`, `gemini-file-search`, `google-docs-export-worker`, `n8n-converter`, `concierge`, `agentic-agent`, `universal-agent` | `platform` (`google-docs-export-worker`: `connection` · `google`) |
| **ai** (3) | `llm-agent`, `vision-agent`, `skill-agent` | `platform` |
| **google-workspace** (25) | `google-gmail`, `google-calendar`, `google-drive`, `google-docs`, `google-sheets`, `google-slides`, `google-forms`, `google-tasks`, `google-contacts`, `google-chat`, `google-meet`, `google-keep`, `google-classroom`, `google-admin`, `google-vault`, `google-analytics`, `google-search-console`, `google-ads`, `google-merchant-center`, `google-business-profile`, `google-books`, `google-youtube`, `google-translate`, `cloud-natural-language`, `google-workspace-assistant` | `connection` · `google` · OAuth (`google-books`, `google-translate`, `cloud-natural-language`: `platform`) |
| **microsoft-365** (12) | `microsoft-outlook-mail`, `microsoft-outlook-calendar`, `microsoft-outlook-contacts`, `microsoft-onedrive`, `microsoft-sharepoint`, `microsoft-teams`, `microsoft-todo`, `microsoft-planner`, `microsoft-onenote`, `microsoft-excel`, `microsoft-word`, `microsoft-powerpoint` | `connection` · `azure` · OAuth |
| **coding-agents** (1) | `gitlab-agent` | `connection` · `gitlab` · OAuth (a personal access token works too) |
| **communication** (2) | `resend`, `cloudflare-email-agent` | `platform` — `send`/`send_batch` gated |
| **media** (2) | `image-gen-agent`, `video-gen-agent` (`generate_video` gated) | `platform` |
| **financials** (1) | `sec-agent` | `platform` |
| **entertainment** (2) | `sports-schedule-agent`, `tv-listings-agent` | `platform` |
| **ecommerce-agents** (2) | `amazon-shopping-agent`, `shopping-zinc` (`buy` gated) | `platform` — `amazon-shopping-agent` also needs an Amazon retailer login, made in Studio |
| **payment** (2) | `x402-wallet-agent` (`fetch_paid` gated), `crossmint-agent` | `x402-wallet-agent`: `connection` · `x402-wallet` · wallet; `crossmint-agent`: `platform` — guarded category |
| **blockchain** (2) | `circle-agent`, `circle-wallet-agent` | `platform` — the Circle wallet is connected in Studio; guarded category |
| **shopping** (1) | `x402-broker` (`execute` gated) | `platform` — needs a wallet connected in Studio |

## 6.6 Using tools from your own LLM loop

The tools plane is designed to sit behind any function-calling model — no SDK required:

1. `GET /v1/tools/:id` for the agents you want; turn each contract into a function definition (`name` = agent id, `parameters` = `inputSchema`, one function per `operations[].name` if you want the model to pick operations).
2. Send the definitions to your model as `tools` (any provider — the [models plane](/one-api/models) works too).
3. When the model returns a tool call, `POST /v1/tools/<id>/execute` with `{ "operation", "params" }`.
4. Feed `content` back as the tool result. On `422` feed back `error.message` + `error.detail` — the model fixes its own params. On `403 refused` tell the user a human has to approve that action.

Every hop carries `x-request-id`; keep it in your logs and `GET /v1/traces/:id` shows the run. For a server-side loop that already does all of this (discover → inspect → execute with an approval gate), call the `concierge` agent on the [agents plane](/one-api/agents).
