Kits and approvals: rights for one task, signed by a person
Grants 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.
The path in one view
Section titled “The path in one view”- Define kits and approvers in
config/authz.yaml, next to grants and denies. - 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. - 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. - 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.
- 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.
- Everything is audited: the request, the decision with the address, signature and statement, and every call made under the kit.
Defining kits
Section titled “Defining kits”Kits are defined in the grants file, config/authz.yaml (Grants explains the file):
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, "*:<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, markedgrant_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_rightsgives the kits per tool and the smallest set that covers a task.
What the agent sees and asks
Section titled “What the agent sees and asks”tell_me_my_rights
Section titled “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,escalatablewith the kits that would allow it, ordenied, 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
Section titled “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 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 the same calls are always there:
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
Section titled “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, a signing address for the approval page, or both.
approvers: key_sha256: "<SHA-256 of the key, 64 hex characters>" address: bc1q0xcqpzrky6eff2g52qdye53xkk9jxkvrh6yhyw kits: [net-diag-read]key=$(openssl rand -hex 32) # the approver's key; hand it over securelyprintf %s "$key" | sha256sum # the key_sha256 for the grants fileaddressis a native segwit P2WPKH address (bc1q…;tb1q…andbcrt1q…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.kitslimits 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 (403on the approver API, an error on the approval page) and recorded in the audit trail as a failedgrant_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 ofapikeys.yamlwith akey_sha256) is refused. Anapikeys.yamlentry with only a bcryptkey_hashcannot 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_granttells 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
Section titled “How a person approves”The approval page
Section titled “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 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
smpprefix). 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=depicks 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
429withRetry-After. A successful approval clears the request’s counters. The counters work like those of 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?
Section titled “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
Section titled “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 |
curl -s -H "Authorization: Bearer $APPROVER_KEY" https://toolmesh.example.com/grants/k3m9x2p7qacurl -s -X POST -H "Authorization: Bearer $APPROVER_KEY" https://toolmesh.example.com/grants/k3m9x2p7qa/approveEach 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
Section titled “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 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 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 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
Section titled “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
Section titled “Operations”TOOLMESH_ISSUERmust 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
tell_me_my_rightsanswersallowedanddeniedonly, andrequest_grantis not listed.