# Kits and approvals: rights for one task, signed by a person

> A kit bundles the tools a task needs, for a limited time. The agent asks with request_grant, a person approves on the approval page with a BIP-322 signature or on the approver API, and the audit trail keeps the signed statement.

Canonical: https://www.toolmesh.io/en/kits/

[Grants](/en/authorization/) cover the daily work: a user, role or plan may call these tools, always. Some tasks need more than that for an hour or an afternoon, and nobody wants to hand out those rights permanently or approve them tool by tool. A **kit** is the answer to that: a named bundle of tools that you define once, that an agent asks for in one step, and that a person grants for a limited time. The approval is the person's own: on the approval page it is a signature by a key only that person holds, and ToolMesh keeps the signed statement in the audit trail. A grant ends by itself.

This page covers the whole path: how a kit is defined, how an agent learns what it lacks and asks, how a person approves, what applies while the grant holds, and what the audit trail keeps. Kits exist in the default `embedded` [authorization mode](/en/authorization/#modes).

## The path in one view

1. **Define** kits and approvers in `config/authz.yaml`, next to grants and denies.
2. **The agent asks what it may do** with `tell_me_my_rights`, over the tools a task needs. The answer names the tools it cannot call and the smallest set of kits that would cover them.
3. **The agent asks for a kit** with `request_grant` (kit, reason, duration). It gets a request id, a fingerprint and, with a signing approver configured, a link to the approval page. It hands the link and the fingerprint to the person it works for.
4. **A person decides.** On the approval page they see what the kit allows, keep what the task needs, pick a duration up to the kit's maximum and sign a statement of exactly that. Or an approver calls the approver API with a key of their own.
5. **The grant holds** for the requesting user on the connection it asked from, until it expires or is revoked. Calls the kit covers pass authorization and, with the exceptions below, the pre-execution gate.
6. **Everything is audited:** the request, the decision with the address, signature and statement, and every call made under the kit.

## Defining kits

Kits are defined in the grants file, `config/authz.yaml` ([Grants](/en/authorization/#grants-configauthzyaml) explains the file):

```yaml
kits:
  net-diag-read:
    description: "Network diagnosis (read-only)"
    warning: "Production systems."
    grants: [graylog:read, unifi:read, netbox:read, checkmk:read]
    tools: [netbox_get_device]
    ttl_default: 4h
    ttl_max: 24h
```

| Field | Meaning |
|-------|---------|
| `description` | Required. What the kit is for, in plain language. Callers see it, and it is what the approver reads. |
| `warning` | Shown with the description. |
| `grants` | Scopes the kit covers: `"*"` every tool, `<backend>` every tool of a backend, `<backend>:<access>` the tools of a backend with this [DADL access classification](/en/authorization/#dadl-access-classes), `"*:<access>"` the tools with this classification on any backend. Quote scopes that start with `*`. |
| `tools` | Single tools by their full name, `<backend>_<tool>`. |
| `ttl_default`, `ttl_max` | How long a grant of the kit lasts by default and at most (`10m`, `4h`); the maximum holds for what the agent asks for and for what an approver grants. A kit without `ttl_max` is granted for at most 24 hours, so its `ttl_default` must stay below that. |

Kit names are slugs: letters, digits, `.`, `_` and `-`, at most 64 characters. `<backend>:<access>` covers exactly the tools of that backend with that classification: holding a kit for `graylog:read` and one for `unifi:write` does not add up to `graylog:write`. Changes to the file apply within seconds, like grants; an invalid kit is reported like an invalid grant.

A kit allows nothing by itself. It changes what callers are told, though:

- Discovery (`discover_tools`, `toolmesh.discover`, `toolmesh.describe`) lists the tools a kit covers next to those the caller may call, marked `grant_required: kit <name>`. A tool no kit covers stays hidden from a caller who may not call it, so define kits only for tools whose names may be seen.
- A denied call names the kits that would allow it, and the audit entry of the denial records them (`remedy_kits`).
- `tell_me_my_rights` gives the kits per tool and the smallest set that covers a task.

## What the agent sees and asks

### `tell_me_my_rights`

The meta-tool `tell_me_my_rights` is listed next to `discover_tools` and `execute_code`. It tells the caller what it may do and decides nothing; every call is checked again when it runs.

- Without arguments: per backend, how many tools the caller may call, how many a kit would add, and which kits.
- `backend: "<name>"`: the verdict for each tool of that backend.
- `tools: [...]`, the preflight (at most 500 tools): one verdict per tool, `allowed`, `escalatable` with the kits that would allow it, or `denied`, and the smallest set of kits found that covers every escalatable tool. An agent that checks the tools of a task first can then ask once for what it lacks, instead of collecting denials call by call.

Every answer also lists the kits the caller holds (`grants`, with id, expiry and time remaining) and its pending requests (`requests`, each with its approval link and fingerprint, so that a link can be handed over later), and describes these kits with their warnings. The answer shows only the tools the caller may call or get through a kit; for any other tool it says `denied`, without telling whether the tool exists. Every call is recorded in the audit trail (tool `tell_me_my_rights`, backend `toolmesh`): it is the first thing an agent that was taken over would look up.

### `request_grant`

`request_grant({kit, reason, ttl})` asks for a kit.

| Argument | Meaning |
|----------|---------|
| `kit` | The kit's name, as `tell_me_my_rights` names it. |
| `reason` | Required, at most 500 characters: the task and, if there is one, the ticket. Approvers see it marked as the requester's own statement. Line breaks become spaces; control and format characters (bidirectional overrides, zero-width characters) are removed. |
| `ttl` | Optional, such as `30m` or `4h`: how long the caller needs the kit. Without it the kit's `ttl_default` applies, or one hour. No more than the kit's `ttl_max` is granted, or 24 hours when it sets none; the maximum counts as it is when the request is approved. The approver may grant the kit for less or more than asked, up to that maximum. |

The answer carries a request id of ten characters and a fingerprint such as `0873-D5E5-9D1B-2DB7`. When an approver has a signing address and `TOOLMESH_ISSUER` is set to the address ToolMesh is reached at, it also carries `approval_url`, the link to the [approval page](#the-approval-page). The answer's `note` repeats the link and the fingerprint word for word, so that an agent which passes the note on passes the link on. A request waits 30 minutes for a decision, then lapses. Asking again for a kit that is still pending returns the pending request; a caller may have at most five requests pending at a time. With the request, ToolMesh records for each entry of the kit how many tools it covers and how many of them the caller cannot call yet; approvers see these counts.

ToolMesh lists `request_grant` as an MCP tool only while kits are defined, and many clients keep the tool list they got when they connected. Inside [Code Mode](/en/code-mode/) the same calls are always there:

```javascript
const rights = await toolmesh.tell_me_my_rights({
  tools: ["graylog_search", "unifi_list_clients", "netbox_get_device"]
});
if (rights.kits && rights.kits.length) {
  const req = await toolmesh.request_grant({
    kit: rights.kits[0].name,
    reason: "Trace the VLAN mismatch from ticket 4711",
    ttl: "2h"
  });
  return req.note;   // link and fingerprint, to hand to the person
}
```

## Approvers

Approvers are listed in the grants file. Each has a name and a key of its own: the SHA-256 of a bearer key for the [approver API](#the-approver-api), a signing address for the [approval page](#the-approval-page), or both.

```yaml
approvers:
  - name: alice@example.com
    key_sha256: "<SHA-256 of the key, 64 hex characters>"
  - name: bob@example.com
    address: bc1q0xcqpzrky6eff2g52qdye53xkk9jxkvrh6yhyw
    kits: [net-diag-read]
```

```bash
key=$(openssl rand -hex 32)   # the approver's key; hand it over securely
printf %s "$key" | sha256sum  # the key_sha256 for the grants file
```

- **`address`** is a native segwit P2WPKH address (`bc1q…`; `tb1q…` and `bcrt1q…` work as well). The approver signs with its private key; ToolMesh stores only the address and never sees the key. No coins and no blockchain are involved: the key only signs statements. Make one on the approval page under "Set up a signing key" (`https://<toolmesh>/approve/`), or use the address of a native segwit account of a wallet that signs BIP-322 messages. Taproot (`bc1p…`) and script addresses are refused.
- **`kits`** limits an approver to the named kits, which must be kits the file defines; without it an approver may approve every kit. An approval of another kit is refused (`403` on the approver API, an error on the approval page) and recorded in the audit trail as a failed `grant_approve`. The approval page offers only approvers who may approve the request's kit. Denying and revoking are open to every approver, as they only take rights away.
- **Approver keys are not MCP credentials.** They work only on the approver API, and the credentials MCP callers connect with do not work there, so an agent cannot approve its own request. Do not give an agent an approver key or a signing key. An approver whose key is also an MCP credential (`TOOLMESH_API_KEY`, `TOOLMESH_AUTH_PASSWORD`, an entry of `apikeys.yaml` with a `key_sha256`) is refused. An `apikeys.yaml` entry with only a bcrypt `key_hash` cannot be compared, so never reuse such a key either.
- Names are slugs or e-mail addresses; neither names, keys nor addresses may repeat. Without approvers, `request_grant` tells callers that no kit can be granted, and ToolMesh logs a warning when the file defines kits.

Changes to `approvers:` apply within seconds. An edit that cannot be applied, because the file does not parse or an approver is refused, stops the server at startup; later it is logged and the previous approvers stay in force. After removing an approver key that may have leaked, check that the log shows `authorization policy applied` for that edit with the new number of `approvers`; until then the old key still approves.

## How a person approves

### The approval page

`https://<toolmesh>/approve/<id>` shows a pending request to whoever has the link and lets an approver with a signing address approve it. The page needs no login: approving takes a signature by the key of an approver's address, and nothing less.

Try it in your browser: the [demo of the approval page](/demo/approval/?lang=en) takes a request of example data from review to approval, in English, German, French, Spanish and Japanese. It signs nothing.

- **What it shows** comes from ToolMesh, the plain answers first: whom the grant is for (user and connection), the risk of what the kit allows (high, elevated, medium or low, after the most far-reaching access class among the entries kept), how many actions would be new to the caller, the duration and the fingerprint. Compare the fingerprint with the one the agent reported, over the channel you work with it in, to know that the link shows its request. Then come the kit's description and warning, and the reason and the client name, marked as the requester's own statements. Under "Technical details" are the request's id and times and the kit's entries, grouped by what they allow (every access class first, then `dangerous`, `admin`, other, `write`, `read`), with the tools each covers and how many of them are new to the caller.
- **Narrowing.** Keep the kit as requested, take it without its dangerous entries, or switch off single entries under "Technical details". The grant then covers only the entries that stay on.
- **Duration.** The page offers the duration requested and steps from 5 minutes to 24 hours, up to the kit's `ttl_max` (24 hours without one). "Other" takes whole minutes or hours typed in, and a hint names the maximum. A grant may be shorter or longer than requested, never longer than the maximum.
- **The statement.** For these choices ToolMesh writes a statement: plain text that names this ToolMesh, the request and its nonce, the approver and address, the kit, the entries granted and left out, the duration, whom the grant is for and the reason. The page shows it as it is, under "Show the exact text you sign", and it is what is signed. ToolMesh writes it again from the stored request to verify the signature, so nothing the page or the requester adds can change it.
- **Signing.** Paste the private key (WIF or 64 hex characters) and press and hold the button to sign in the page; the key stays in the browser and only the signature is sent. Or copy the statement into a wallet that signs BIP-322 messages and paste the signature (with or without the `smp` prefix). On a page served over plain HTTP (other than localhost) the key field is disabled: use HTTPS.
- **Languages.** The page speaks English, German, French, Spanish and Japanese. It follows the browser's language; `?lang=de` picks one, and the language switch keeps the choice in the browser. The statement stays English: it is what ToolMesh verifies and records. In the other languages the page shows a reading aid beside it, line by line, which is not part of what is signed. The kit's description and warning appear as the grants file has them, and the requester's reason is never translated.
- **Once.** A signature approves its request once: approving takes the request from the store, and a statement names one request and its nonce.
- **Limits.** Refused approvals (a signature that does not verify, or choices that do not fit the request) are limited within 15 minutes to 5 per request and source address, 10 per request and 20 per source address; beyond that the page answers `429` with `Retry-After`. A successful approval clears the request's counters. The counters work like those of [login throttling](/en/authentication/#login-throttling), with the same source address and in Redis when it is connected, but apart from them: refused signatures never use up the login limits.

The page loads only its own style sheet and scripts, which are part of the ToolMesh binary (Content Security Policy `default-src 'none'`, no other origin, no framing). Messengers that show link previews get a summary from ToolMesh (kit, user, duration, tool counts, fingerprint), never the reason. `/approve/` without an id offers the key setup only.

Set `TOOLMESH_ISSUER` to the address approvers reach ToolMesh at. It is the base of the links in `request_grant` and the ToolMesh a statement names. Without it, `request_grant` gives no link, and statements name the address the page was loaded from.

### What is BIP-322?

BIP-322 is a standard for signing and verifying messages with the key behind a Bitcoin address. ToolMesh uses the signing part and nothing else: an approver's key signs the statement, and ToolMesh checks the signature against the address in `config/authz.yaml`. No coins, no blockchain and no account are involved. The format was chosen because it is specified, because the verification runs with nothing but the address and the statement, and because the key can live in a browser or in a wallet that already signs such messages.

What this gives you: the record of an approval is a signature that only the approver's key could have produced, over a text that says exactly what was granted. Nobody has to trust the gateway's word that a person approved: the audit entry carries the address, the signature and the statement, and anyone with the entry can verify it later. A gateway that records "approved by alice" on its own authority cannot offer that.

### The approver API

On the ToolMesh port, with the approver key as bearer credential:

| Request | Effect |
|---------|--------|
| `GET /grants` | Pending requests and active grants |
| `GET /grants/<id>` | One request or grant |
| `POST /grants/<id>/approve` | Grant the kit. Optional body: `{"ttl": "1h"}` to grant it for another duration than requested, shorter or longer, up to the kit's `ttl_max` (24 hours without one; above it the approval is refused with `400`); `{"grants": ["graylog:read"], "tools": [...]}` to grant only these entries of the kit, exactly as the kit lists them |
| `POST /grants/<id>/deny` | Reject a pending request |
| `POST /grants/<id>/revoke` | End an active grant now |

```bash
curl -s -H "Authorization: Bearer $APPROVER_KEY" https://toolmesh.example.com/grants/k3m9x2p7qa
curl -s -X POST -H "Authorization: Bearer $APPROVER_KEY" https://toolmesh.example.com/grants/k3m9x2p7qa/approve
```

Each request and grant shows the kit's description, warning and scope next to who asked, why, and for how long, and a pending request its fingerprint, tool counts and approval link. Like the reason, `caller_name` is the requester's own statement: an OAuth client chooses its name itself. Approving a kit the caller already holds replaces that grant. A grant keeps its request id, so the same id revokes it, whether it was approved here or on the approval page.

## While a grant holds

**Who holds it.** The requesting user, as the caller it connected as: the `caller_id` of an API key, or the OAuth client. An API key without `caller_id` binds the grant to that key itself (shown as `grant_key`), so two such keys of one user do not share grants. The grant survives a token refresh; other connections of the same user do not hold it.

**What it covers.** A grant of the whole kit with an approver key gives the kit as it is defined at the time of each call: a kit removed from the file allows nothing any more, and a changed kit applies with its new content. A grant of **part of a kit** (some of its `grants` scopes and `tools`), and every grant approved on the approval page, covers the entries the approval named and no others: entries removed from the kit later allow nothing, entries added later are not part of it. A [deny](/en/authorization/#denies) wins over a kit as over every grant; it decides in the authorization step, before the kit is looked at.

**A kit replaces the pre-execution gate.** A call that a held kit covers passes the [gate](/en/output-gate/) on the approver's word: every pre-execution policy without the directive `// toolmesh: enforce-with-kit` in its header comment is skipped for it. Policies with the directive, meant for parameter filters and for conditions that narrow what a scope allows (no force-push to `main`, no turning a device off), still run: a kit grants the tools, not every argument. The post-execution gate (masking, field filters) runs for every call. The call is logged as `kit replaces the pre-execution gate`, and its audit entry carries `kit_applied` and `gate_skipped`. Whether the steady-state grants would have allowed the call as well does not matter: the kit applies whenever it covers the tool. See [Policies and kits](/en/output-gate/#policies-and-kits) for the directive and the `ctx` fields.

**Where grants live.** In Redis when ToolMesh is connected to one (`REDIS_URL`): requests and grants survive a restart and are shared between instances. Otherwise in process memory: a restart ends all requests and grants, and every caller is back to its steady-state rights. A store that fails is logged and contributes no kits.

**Ending it.** A grant expires on its own. `POST /grants/<id>/revoke` ends it now, and the next call no longer holds the kit.

## In the audit trail

Every step is recorded with backend `toolmesh`:

| Tool | When | What the entry carries |
|------|------|------------------------|
| `request_grant` | The agent asks | The user and caller the grant is for; in the metadata the request id, kit, the reason and the duration asked for (`ttl_requested`) |
| `grant_approve` | An approver approves | The approver, `approved_with` (`key` or `signature`), kit, duration and expiry; for part of a kit the entries granted (`part_grants`, `part_tools`); on the approval page also the address, the signature, the statement and its SHA-256 (`statement_sha256`). A refused signature is recorded as an error |
| `grant_deny`, `grant_revoke` | An approver denies or revokes | The approver, the grant id and the kit |
| `tell_me_my_rights` | The agent asks what it may do | Every call |
| any tool | A call made while the caller holds kits | `held_kits` (a kit held in part marked `(part)`); when one of them covers the tool, `kit_applied` names it and `gate_skipped` the pre-execution policies it replaced |
| any tool | A call denied | `remedy_kits`, the kits that would have allowed it |

With `AUDIT_STORE=log` the metadata is a `metadata` object in the audit line; the `sqlite` store keeps it in the entry. To verify an approval later, take `address`, `statement` and `signature` from its entry and check the signature with any BIP-322 verifier.

## Operations

- **`TOOLMESH_ISSUER`** must be the public address of ToolMesh: it makes the approval links and names the ToolMesh in every statement.
- **HTTPS** for the approval page; the key field is disabled on plain HTTP away from localhost.
- **Redis** if grants are to survive a restart or to be shared by several instances.
- **Approver keys and signing keys** stay with people. Never put one into an agent's configuration, and never reuse an MCP credential as one.
- **Kits apply in embedded mode only.** In [external mode](/en/authorization/#external-mode) `tell_me_my_rights` answers `allowed` and `denied` only, and `request_grant` is not listed.
