Repository navigation
ToolMesh 0.5.0 lets an agent ask for the rights a task needs, and lets a person grant them with a signature of their own. A kit bundles the tools for a task. The agent asks for it in one step, an approver signs the approval on the approval page, and the grant ends by itself. Authorization is now on by default, with OpenFGA running inside ToolMesh.
Upgrading from 0.4.x: read the behavior changes below first. Authorization is now on, a server without a credential rejects every request, and /mcp answers 401 instead of 200 without a valid credential.
Kits and approvals signed by a person
- Kits. A kit in
config/authz.yamlis a named bundle of tools a caller is granted as a whole and for a limited time: scopes such asgraylog:read, single tools, a description, an optional warning, and a default and maximum duration. Discovery lists the tools a kit covers, markedgrant_required, and a denied call names the kits that would allow it. - Asking.
tell_me_my_rightstells a caller what it may do, or checks the tools a task needs in advance and names the smallest set of kits that covers what is missing. It takes the pre-execution policies into account, so a tool a policy would refuse shows up as needing a kit, or as denied with the policy named, instead of failing at the gate later.request_grantasks for a kit with a reason and an optional duration. Both work fromexecute_codescripts as well. The answer carries a link to the approval page and a fingerprint of the request, and tells the agent to pass both on exactly as written. - Approving with a signature. On the approval page an approver approves by signing a statement that ToolMesh writes for the request (BIP-322, from a
bc1q…address). The signature is made in the page, where the private key never leaves the browser, or in a wallet. The approver sees the kit's entries grouped by what they allow, can untick entries and shorten the duration, and the grant covers exactly what was signed. The audit trail keeps the address, the signature and the signed statement. The signature comes from the approver's key, not from ToolMesh, so anyone can check it later against the approver's address. Refused signatures are limited per request and source address. - Approver API.
/grantsapproves, shortens, denies and revokes with a key of the approver's own, also for part of a kit; MCP credentials do not work there. Approvers can be limited to certain kits. - While a grant holds. A kit the caller holds replaces the pre-execution gate for the tools it covers, and a call the gate refuses names the kits that would lift the refusal. Policies that must hold for kit holders as well, such as parameter filters, declare
// toolmesh: enforce-with-kitand keep running. Denies inconfig/authz.yamlstill win over a kit, and the post-execution gate runs in full. - Audit. Every request, approval, denial and revocation is audited, and so is every call a kit allowed.
Documentation: Kits and approvals.
Behavior changes: read before upgrading
- Authorization is on by default. ToolMesh runs OpenFGA in its own process (
TOOLMESH_AUTHZ_MODE=embedded) and keeps its data inauthz.dbin the data directory; no OpenFGA service is needed. On a fresh installation the owner and the roleadminmay call every tool; withusers.yamlorapikeys.yaml, planpromay call every tool and planfreeevery tool classifiedread. Any other caller gets nothing until a grant names it. With the defaults of the single password or API key nothing changes.
An installation that ran without authorization is recognized on the first start of 0.5.0 and stays open to every authenticated caller (presetopen). The security-posture summary reports this at every start until you choose a preset (TOOLMESH_AUTHZ_PRESET) or write grants inconfig/authz.yaml. To begin with the restrictive defaults instead, setTOOLMESH_AUTHZ_MODE=embeddedbefore that first start.OPENFGA_MODEis deprecated;restrictkeeps working asTOOLMESH_AUTHZ_MODE=external. - A server without a credential rejects every request. Without
TOOLMESH_AUTH_PASSWORD,TOOLMESH_API_KEY,users.yamlorapikeys.yaml, requests to/mcp,/files/uploadandDELETE /blobs/…get401. Up to 0.4.1 they were served anonymously. /mcpanswers401withWWW-Authenticateinstead of200when a request has no valid credential; the JSON-RPC error in the body is unchanged. MCP clients use this status to start the OAuth flow or to refresh an expired token. A credential that cannot be checked at that moment gets503withRetry-After(on/mcp,/authorize,/files/uploadandDELETE /blobs/…)./authorize: a wrong password gets401instead of200, a throttled login429withRetry-After, an unregistered client or redirect URI400before the password is checked, and a server without a configured password403.- Metrics:
toolmesh_logins_totalhas the new valuesmethod="password"andmethod="anonymous", and itsfailureseries foroauth_bearerandapi_keynow count rejected bearer credentials. - Telemetry carries a site ID, a random UUIDv7 kept in
toolmesh.jsonin the data directory, so the statistics count installations rather than addresses.DO_NOT_SEND_ANONYMOUS_STATISTICS=yescreates no site ID and sends nothing; to discard an existing one, stop ToolMesh and removesite_idfromtoolmesh.json. - New settings:
TOOLMESH_AUTHZ_MODE,TOOLMESH_AUTHZ_PRESET,TOOLMESH_AUTHZ_CONFIG, the login limitsTOOLMESH_LOGIN_MAX_FAILURES_PER_USER_IP,…_PER_USER,…_PER_IPandTOOLMESH_LOGIN_FAILURE_WINDOW(an invalid value stops the server at startup),TOOLMESH_BCRYPT_MAX_CONCURRENT, and the fieldkey_sha256inapikeys.yaml.
Upgrade checklist
- Keep the data directory writable and persistent; it now holds
authz.db,.authz-bootstrapandtoolmesh.json. - After the first start, check the log line
authorization policy appliedand choose a preset or grants if the installation came up asopen. - A server that ran without any credential needs
TOOLMESH_API_KEYorTOOLMESH_AUTH_PASSWORD. - Add
key_sha256to every entry inapikeys.yamlwhose key was generated randomly. The keys stay the same, andkey_hashmay stay next to it. ToolMesh warns at startup while entries have only akey_hash. - Set
TOOLMESH_ISSUERto the public URL clients use. It is now part of the401challenge, the protected resource metadata and the approval links, and ToolMesh warns at startup while it is the placeholder. - A reverse proxy must pass
401,403,429and theWWW-Authenticateheader on/mcpand/authorizethrough unchanged. - A login shared by many people, such as a public demo account, should set
TOOLMESH_LOGIN_MAX_FAILURES_PER_USER=0and keep the per-address limits.
Other changes
- Grants and denies in
config/authz.yamlname users, roles, plans, companies or everyone, and allow all tools, the tools of a backend, the tools of a DADL access class or single tools; changes apply within seconds.TOOLMESH_AUTHZ_MODE=disabledis the only way to switch authorization off, and it is reported at startup and recorded in the audit trail. Documentation: Authorization. - New example policy
prod-access-by-caller-class.js: lets a call through only when the caller's class may use the tool's DADLaccessclass.discover_toolsandtoolmesh.describe()now show thataccessfield. - The
logaudit store writes an entry's metadata as ametadataobject, as thesqlitestore already did. - Installations with more than 50 DADL files now get their telemetry through; reports are split into requests of at most 50 entries.
- Building ToolMesh requires Go 1.26; the embedded OpenFGA is v1.22.0.
Security
/authorizerefuses logins when no password is configured; until now an empty password was accepted.- Failed password logins are limited per account and client address, per account, and per client address.
- A bearer credential no longer costs one bcrypt comparison per configured API key; API keys and access tokens are looked up first, and bcrypt comparisons are bounded for the whole process.
- A server without any configured credential no longer serves requests anonymously.
Full details in CHANGELOG.md.
Docker image: ghcr.io/dunkelcloud/toolmesh:0.5.0, also tagged 0.5 and latest, built for linux/amd64 and linux/arm64. Image tags carry no v prefix.