# Private tools

## What a tool sees

| Placeholder | Value |
|---|---|
| `$auth.subject` | The token's `sub` |
| `$auth.token` | The raw JWT, to forward to your API |
| `$auth.claims.<name>` | Any custom claim |

```yaml
# tools/my_bookings.yml
description: The visitor's upcoming bookings.
when_to_use: When they ask about their own bookings.
access: private
request:
  method: GET
  base_url: $env.API_URL
  path: /api/bookings
  headers:
    Authorization: Bearer $auth.token
respond:
  data:
    bookings: bookings
```

Your endpoint verifies the same JWT with the same public key, and scopes the
query to its `sub`.

## Verifying on your endpoint

Vatio verifies the token before it unlocks a private tool, but that does not
protect your API: the endpoint is on the internet, and anyone can call it with
a made-up header. Verify the forwarded token on every request and check:

- **Signature**, with the public half of the key you sign with, and the
  algorithm pinned (`ES256` or `RS256`), never read from the token's header.
- **`aud`** is your workspace slug, so a token signed for another workspace is
  refused.
- **`exp`** has not passed.

Then take the user from `sub`, and from nothing the request body says. A token
that fails any check is a `401`.

In Rails, [`vatio-sdk`](https://vatio.ai/docs/sdk/ruby) does all of it:

```ruby
class Api::BookingsController < ApplicationController
  include Vatio::Identity::Authentication
  before_action :authenticate_vatio!   # 401 unless the token verifies

  def index
    render json: Booking.where(user_id: vatio_subject)
  end
end
```

If your app serves several workspaces, each with its own key, build a
`Vatio::Identity::Config` per workspace and return it from
`vatio_identity_config` in the controller; see
[Several workspaces](https://vatio.ai/docs/sdk/ruby#several-workspaces).

Anywhere else, any JWT library will do. In Node, with
[`jose`](https://github.com/panva/jose):

```js
import { importSPKI, jwtVerify } from "jose";

const key = await importSPKI(process.env.VATIO_IDENTITY_PUBLIC_KEY, "ES256");

app.get("/api/bookings", async (req, res) => {
  const token = req.headers.authorization?.replace(/^Bearer /i, "");
  try {
    const { payload } = await jwtVerify(token, key, {
      audience: "acme",          // your workspace slug
      algorithms: ["ES256"]
    });
    res.json(await bookingsFor(payload.sub));
  } catch {
    res.status(401).json({ error: "unauthorized" });
  }
});
```

Nothing to verify when no token is sent: public tools carry no `$auth`. On
WhatsApp and Instagram a private tool sends the token the contact signed in
with ([On WhatsApp and Instagram](https://vatio.ai/docs/authentication/sessions#on-whatsapp)).

## Rules

**Never take the user's id as a parameter.** The model fills parameters, and it
can be talked into filling that one with someone else's id. A private tool
declaring `user_id`, `customer_id`, `account_id`, `member_id`, `patient_id` or
`subject` fails the deploy:

```text
tool my_bookings: a private tool cannot take "user_id" as a parameter — the model
fills parameters and can be talked into filling that one with someone else's id.
Use $auth.subject, which comes from the verified token
```

Derive the user from the token instead. An endpoint that accepts an id has to
remember to scope it every time, forever; one that reads the token cannot forget.

Other errors you may hit:

| Error | Fix |
|---|---|
| `access: private needs an auth: block` | Add `auth:` with `public_key:` |
| `auth.public_key ... is a PRIVATE key` | Point at `identity.pub`, not `identity.pem` |
| `auth.algorithm must be one of RS256...` | Sign with RS256/ES256, not HS256 — a shared secret cannot live in a manifest |
| `access must be "public" or "private"` | Scheme names are gone; use `private` |
| `auth/ holds JavaScript auth providers, which are gone` | Delete `auth/`, sign a JWT instead |
| `JavaScript tools are gone` | Delete `tools/*.js` and `lib/*.js`; describe the call in `tools/<key>.yml` |

A rejected token — wrong key, expired, wrong `aud` — leaves the visitor
anonymous rather than erroring, and the agent falls back to its public tools.
`vatio chat` shows the reason.
