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

# Tools plane — /v1/tools

> Every active agent is also callable as a tool with a machine-checkable invocation contract. The flow is always: discover → inspect the contract → execute. This plane runs the platform's trust fence — gated and money operations refuse unattended execution by design.


<!-- sources-of-truth: orchestration/api-gateway/openapi.ts, docs/ONE-API-DEVELOPER-GUIDE-2.0.md | last-synced: 2026-09-24 (re-verified against orchestration/api-gateway/routes/tools.ts, _util.ts, core/core-base/call-agent.ts; fixed a stale "Settings → Integrations / Connectors" UI label to the real one, Studio → Connectors, per apps/ocean-studio/src/components/connectors/) -->
Every active agent is also callable as a **tool** with a machine-checkable
invocation contract. The flow is always: **discover → inspect the contract →
execute**. This plane runs the platform's trust fence — gated and money
operations refuse unattended execution by design.

Auth: `Authorization: Bearer ozk_YOUR_KEY` on every route here.

## GET /v1/tools — list active tool-agents

Query params: `limit` (default 50, max 100), `offset` (default 0).

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

```json
{
  "tools": [
    { "id": "brave-agent", "name": "Brave Search", "type": "agent",
      "category": "search", "status": "active",
      "description": "Web search via Brave", "tags": ["search"] },
    { "id": "pdf-agent", "name": "PDF Agent", "type": "agent",
      "category": "documents", "status": "active",
      "description": "Convert and extract PDFs", "tags": ["pdf"] }
  ],
  "count": 2
}
```

`count` is the returned page's length, not the catalog total — page until a
short page, never until `offset >= count`. A failed registry hop returns
`502 upstream_error`.

## GET /v1/tools/{id} — the invocation contract

Fetch this **before the first execute** of any unfamiliar tool. The contract
tells you the operations, which are gated, the required params, the JSON
Schema for inputs, and worked examples.

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

```json
{
  "agent_id": "brave-agent",
  "name": "Brave Search",
  "description": "Web search via the Brave Search API",
  "operations": [
    { "name": "search", "purpose": "Run a web search", "gated": false,
      "required_params": ["query"] },
    { "name": "news", "purpose": "Search recent news", "gated": false,
      "required_params": ["query"] }
  ],
  "inputSchema": {
    "type": "object",
    "properties": {
      "query": { "type": "string" },
      "count": { "type": "integer" }
    }
  },
  "examples": [
    { "title": "Basic web search", "cnl": "operation: search\nparams:\n  query: cloudflare workers" }
  ],
  "output_type": "markdown"
}
```

404 → `{"error":{"code":"unknown_tool","message":"..."}}` — the id isn't an
active tool. Re-discover via `GET /v1/tools` or discovery search.

`gated: true` on an operation means executing it unattended is refused with
**403 `refused`** by this route's pre-dispatch fence (`checkAgentCall` runs
in `'unattended'` mode here, which always resolves a gated-verb/guarded-
category hit to a refusal, never a pause). `409 approval_required` is a
*different*, real code path on this same route — see the fence section below.

## POST /v1/tools/{id}/execute — execute an operation

Body — two styles, both valid:

| Field | Type | Notes |
|---|---|---|
| `operation` | string | Operation name from the contract |
| `params` | object | Params per the contract's `inputSchema` |
| `prompt` | string | Free-text alternative for prompt-driven tools |
| `format` | string | Response negotiation: `json` (default) \| `yaml` \| `markdown` \| `text` — same values as an `Accept` header or the request `Content-Type` mirror |

Validation rules ("reserved" = param names colliding with engine task-envelope
fields — `agent`, `type`, `id`, `operation`, `depth`, `user_id`,
`executor_user_id`, `workflow`, `execution_id` — never forwardable):

- Reserved param names → 422, always.
- Missing required params → 422 **only when no `prompt` is present** — a prose
  prompt satisfies prompt-capable agents, so a prompt-style call skips param
  validation entirely. The trust fence still runs either way.

Structured style (preferred when the contract defines operations):

```bash
curl -s -X POST https://api.plungeai.com/v1/tools/brave-agent/execute \
  -H "Authorization: Bearer ozk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"operation": "search", "params": {"query": "cloudflare workers pricing"}}'
```

Prompt style:

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

Success — HTTP 200:

```json
{
  "ok": true,
  "content": "# Web Search: cloudflare workers pricing\n\n1. ...",
  "outcome": "success",
  "request_id": "00000000-0000-4000-8000-000000000004"
}
```

## Error semantics — features, not failures

| Status | Code | Meaning | Correct handling |
|---|---|---|---|
| 400 | `invalid_format` | `format` isn't one of `json`/`yaml`/`markdown`/`text` | Fix the field |
| 404 | `unknown_tool` | No such active tool | Re-discover the id |
| 422 | `invalid_params` | Contract violation — response body carries `missing`, `reserved`, `warnings`, **and the full contract** | Self-correct from the echoed contract; retry once with fixed params |
| 403 | `refused` (fence) | Gated operation, or payment/blockchain-category agent — never callable unattended | **FINAL. Surface verbatim. NEVER retry, rephrase, or work around** |
| 403 | `refused` (lifecycle) | Parked agent (`status:developing/development/developed`) or unknown card — the `reason` says which | Re-discover via `GET /v1/tools` / discovery search |
| 409 | `approval_required` | The dispatched run's own outcome came back `needs_approval` (mapped 1:1 via the outcome→HTTP table — guide 3.0 §13.3/§13.5). NOT produced by the pre-dispatch gated-verb/guarded-category fence, which always resolves to 403 on this route (see below) | Approve out-of-band (Studio / an attended surface), then re-issue the identical request |
| 409 | `duplicate_execution_id` | The `x-trace-id` header was already used by an earlier run | Send a fresh UUID |
| 413 | `payload_too_large` | Body over 1 MiB (`MAX_REQUEST_SIZE`) | Shrink the payload (e.g. stream large files through a URL param instead of inline `file_data`) |
| 424 | `connection_required` / `credential_required` | The agent needs a connection or key the acting user hasn't set up | Tell the user what to connect (Studio → Connectors); retry after |
| 429 | `rate_limited` | Per-key limiter — `Retry-After` header | Back off and retry |
| 502 | `execution_failed` | The dispatched agent failed — body carries `outcome`, `detail`, `content` | Act on `outcome` (`needs_input`, `needs_connection`, …) before blind-retrying; one retry only for transient outcomes |
| 502 | `upstream_error` | Registry/engine hop threw (any route in this plane) | One retry with backoff |
| 503 | `agent_unavailable` | Agent temporarily down | Retry later or pick an alternative |

### 422 example — the contract comes back to you

```json
{
  "error": {
    "code": "invalid_params",
    "message": "params failed card validation",
    "missing": ["query"],
    "reserved": [],
    "warnings": [],
    "contract": { "agent_id": "brave-agent", "operations": [ "..." ] }
  }
}
```

Generated code should parse `missing` and the embedded `contract`, repair the
body, and retry exactly once. If the second attempt also 422s, stop and report.

### 403 — the trust fence, and how to tell refusals apart

**Fence refusals are FINAL.** The gated-verb floor (`buy`, `purchase`,
`fetch_paid`, `send`, `transfer`, `pay`, `send_payment`, `withdraw`, `shop`),
operations the card tags `gated:`, and payment/blockchain-category agents
(refused **regardless of operation** on this surface):

```json
{ "error": { "code": "refused", "message": "operation \"transfer\" on x is gated (outward/irreversible) and requires human approval — not available in unattended runs." } }
```

Do not loop, do not reword the prompt to sneak past the fence, do not switch
to the prompt style to avoid the operation gate. Tell the user what was
refused and why; approvals happen on attended surfaces (Ocean Studio / apps),
not through this API.

**Lifecycle refusals are not final** — the same 403 `refused` code also covers
parked agents ("parked (status:…) — not callable. Use registry search to find
an active alternative.") and ids with no registry card. The `reason` string
distinguishes them; the remedy is re-discovery, not surrender.

**`409 approval_required` is real and reachable on this route** — but not
from the check above. `POST /v1/tools/{id}/execute` calls the shared
gated-verb/guarded-category fence (`checkAgentCall`, `core/core-base/
call-agent.ts`) in `'unattended'` mode, and in that mode every branch that
could otherwise pause for approval (a guarded payment/blockchain category
with no recognizable operation, or a gated verb) resolves to `403 refused`
instead — `approval_required` is only returned by that function in
`'interactive'` mode, which this route never uses. The 409 you actually see
on this plane comes from a **different** source: the dispatched run's own
outcome. If the downstream agent/tool run itself pauses mid-execution
(outcome `needs_approval` — the same status MCP surfaces as a ⏸ block), the
outcome→HTTP mapping (guide 3.0 §13.3, "Trust fence" §13.5) turns that into
`409 approval_required` here too. Handling: **approve out-of-band** (Studio
or another attended surface), **then re-issue the identical request** — the
One API has no continuation token like MCP's `plungeai_continue`, so you
resend rather than resume.

## Tools vs agents — which plane to call

- `/v1/tools/:id/execute` — structured, contract-validated, fenced. Use when
  you know the operation and params (typical for generated app code).
- `/v1/agents/:id/execute` — free prompt, sync/async semantics, no operation
  contract. Use for open-ended instructions to LLM-driven agents.

Same underlying agents, different invocation disciplines.
