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

# Identity & errors — whoami, the auth model, outcome envelope, trust fences

> ---


<!-- sources-of-truth: orchestration/mcp-gateway/tools.ts, orchestration/mcp-gateway/README.md, orchestration/mcp-gateway/extras.ts, orchestration/mcp-gateway/auth-middleware.ts, orchestration/mcp-gateway/index.ts, orchestration/mcp-gateway/tool-outcome.ts, orchestration/mcp-gateway/server.ts | last-synced: 2026-09-24 (re-verified: auth order + unsupported_bearer_token against auth-middleware.ts, 20-tool catalog against server.ts TOOL_DEFS, rate limits against errors doc — all match; fixed a stale "Dashboard → API Keys" UI label to Dashboard → One API → Keys per apps/ocean-dashboard/src/nav.ts) -->
---

## plungeai_whoami

**Purpose:** show the authenticated identity — the ground truth for "who am
I acting as" and "how much budget is left". Read-only, no arguments beyond
`user_request`. Use it to verify a new connection, and whenever ownership
("Execution not found") or rate-limit errors appear.

**Example:** `{"user_request": "verify my plungeai connection"}`

**Returns**

```
# Who am I
- **User:** `<uuid>`
- **Auth:** api-key · tier **pro**
- **Key:** <label> (`<key id>`)
- **Rate window:** N/min used · M/day used
- **Server:** plungeai.com (mcp.plungeai.com)
```

**Failures & fixes:** "Authentication required." → the key never reached the
server; fix the header (`Authorization: Bearer ozk_YOUR_KEY` or
`X-API-Key: ozk_YOUR_KEY`). The rate window line is best-effort and may be
absent — that alone is not an error.

---

## The auth model

**An API key IS a user identity.** The key resolves to a user id, and every
read/write on every tool is scoped to what that user owns — the same view
that user has in Studio, never the whole system. Every id-based operation
(executions, workflows, jobs, conversations, memory) is ownership-checked
before any storage is read; a foreign id answers "not found", not "forbidden"
— so a plausible-looking id that returns "Execution not found" usually
belongs to someone else or is mistyped.

Auth order: an internal service path an API key can never reach →
`X-API-Key` → `Authorization: Bearer` (personal API keys, e.g.
`ozk_YOUR_KEY`; any other bearer value is rejected — logged as
`unsupported_bearer_token` — only `ozk_` keys authenticate, there is no
OAuth token path today). Unauthenticated requests get **401** with a
`WWW-Authenticate: Bearer` challenge (`error="invalid_token"` when
credentials were supplied but invalid). Keys are self-service — mint your
own at **Dashboard → One API → Keys** (`https://dashboard.plungeai.com`); an
account owner can additionally fence (`allowed_ips`/`allowed_tools`) or
revoke any key on the account, including a shared team key.

**Per-key fences** (both optional, set at mint):

- `allowed_ips` — the key only authenticates from listed addresses/CIDRs;
  anywhere else is a 401 (and counts against failed-auth metering).
- `allowed_tools` — other tools are HIDDEN from `tools/list`, and calling one
  returns `-32602 Tool not permitted for this key`. If a documented tool is
  missing from `tools/list`, the key is fenced — re-mint or edit the key's
  fence in the Dashboard; do not keep calling it.

**Rate limits** are per tier: free 30/min · 1k/day, pro 100/min · 10k/day,
enterprise 300/min · 100k/day (tool calls only). Over-limit → HTTP 429 with a
`Retry-After` header — wait that long, then resume; never tight-loop.

**Activity trail:** every request is recorded to a per-user daily log
(arguments in full, answers as receipts). A user reads their own with their
own key: `https://mcp.plungeai.com/log?date=YYYY-MM-DD` (Bearer header, or
`&key=ozk_YOUR_KEY` in a browser). This is also why `user_request` matters:
it is the one place the user's actual sentence survives into diagnostics.

---

## Two error planes

**1. Protocol errors (JSON-RPC, transport-level)** — the request itself was
rejected; no tool ran:

| Signal | Meaning | Your move |
|---|---|---|
| 401 + `WWW-Authenticate` | Missing/invalid/revoked key, or IP outside the key's fence | Fix the credential; don't retry blind. Repeated failures meter the IP (429 with Retry-After). |
| 429 + `Retry-After` | Rate limit (per-tier), or failed-auth metering | Wait the stated seconds, resume, consider batching. |
| `-32602` "Tool not found" / "Tool not permitted for this key" | Typo'd tool name, or a key fence | Re-check against `tools/list`; if fenced, edit the key's fence in the Dashboard. |
| `-32700` Parse error / 413 body too large (1 MiB) | Malformed or oversized request | Fix the payload; huge YAML belongs in a saved workflow, not inline. |
| 405 / 403 invalid Host / `-32603` internal | Transport misuse or server fault | Use POST to the canonical endpoint; internal errors: retry once, then report. |

One deliberate crossover: arguments that fail a tool's own schema (e.g.
`limit: 200`) are pre-screened BEFORE the tool runs and still answer in the
envelope — `needs_input`, "`<tool>` was not called — its arguments do not
match the tool's schema", with `provide_field` actions per bad field. Nothing
ran; fix the named fields against the tool's `inputSchema` from `tools/list`.

**2. Tool outcomes (the envelope)** — the tool ran (or deliberately refused)
and answered with structured guidance. Execution tools NEVER return a raw
error: the answer is text plus `structuredContent` with

```
status: ok | needs_input | needs_connection | needs_api_key
      | needs_approval | unavailable | error
summary, agent_id?, operation?, result?, execution_id?,
missing?[], warnings?[], contract?,
remediation: { message, actions: [...] }
```

Typed `remediation.actions` tell you the exact next move:
`connect_provider` (user opens the named URL and connects — needs their
browser), `provide_api_key` (user saves a key in a PlungeAI app),
`provide_field` (fix that field, retry the same call), `approve_via` /
`respond_via` (the ⏸ loop → `plungeai_continue`), `retry_with` (retry with
the stated changes, e.g. `{mode: "async"}`), `use_alternative` (a real,
callable substitute agent id).

`isError` is true ONLY for terminal `error` outcomes. Every other non-ok
status is guidance the model acts on — the response even ends with "This is
structured guidance, not a crash — do not retry the identical call."
Believe it.

---

## The trust fences (surface, never retry, never bypass)

**The active-only fence.** Only `status:active` agents are listed and
callable over MCP. `plungeai_execute_agent`, `plungeai_execute_tool`,
ad-hoc YAML in `plungeai_execute_workflow` (including agents nested in
`parallel`/`sequential` blocks), and `plungeai_schedule` create all refuse an
unknown or inactive id BEFORE dispatch, writing no execution row. Every
refusal's prose points you at `plungeai_list_agents`; `plungeai_execute_agent`
and `plungeai_execute_tool` additionally attach live `use_alternative`
suggestions to their `unavailable` envelope — `plungeai_execute_workflow`'s
fence refusal carries no remediation actions, and `plungeai_schedule` create
answers with plain error text, not an envelope. A refusal is a policy answer,
not a transient failure: **always re-discover** with `plungeai_list_agents`
(semantic `search`), take a fresh id from the results, and never "retry"
the refused id or guess a variant spelling.

**The approval fence.** Gated operations and real-world side effects pause
as `needs_approval` / `⏸ AWAITING USER APPROVAL` (a paused run also shows it
in `plungeai_get_workflow_status` → `continuation`). This is a deliberate
trust boundary between the platform and the human: relay it verbatim, let
the USER decide, and only then `plungeai_continue {approve: true}` (yes) or
`{message: "<their words>"}` (no / change). Approving on your own, retrying
around the pause, or resubmitting the original call are all violations —
the platform holds the pending action server-side until the user answers.

**Connection fences.** `needs_connection` / `needs_api_key` mean the USER
must act in their browser (connect an account or save a key in a PlungeAI
app, e.g. Studio → Connectors, per the remediation's URL). Relay the exact
instructions, wait for their confirmation, then retry the IDENTICAL call —
this is the one case where the same call is expected to be repeated, and
only after the user acted.

**Ownership answers.** "not found / not one of your executions" on a
plausible id is an ownership verdict, not a storage glitch — re-list
(`plungeai_executions`, `plungeai_list_workflows`, `plungeai_schedule
{action: "list"}`) and use an id the account actually owns.

---

## Failure classification you can rely on

Whatever the pre-flight didn't predict, the engine's raw failure is
classified into the same envelope — a named missing API key becomes
`needs_api_key`, an upstream 401 becomes `needs_connection` naming the
provider to connect, insufficient OAuth scopes become "reconnect to grant
additional scopes", missing/invalid inputs become `needs_input` WITH the
agent's contract attached, an undeployed binding becomes `unavailable` with
alternatives, and a timeout becomes `error` with `retry_with {mode:
"async"}`. A failed run always carries a reason — `plungeai_get_result` on a
failed id answers "This run failed: <reason>" rather than a dead end. So:
read the status, follow the remediation, and reserve blind one-time retries
for the one class labeled transient ("This is a transient platform failure
... Retry once in a few seconds", stamped with a request id to report if it
persists).
