Skip to content

v0.5.0 — Kits and approvals signed by a person

Latest

Choose a tag to compare

@axeldunkel axeldunkel released this 23 Oct 10:35
· 1 commit to main since this release
6c3fd12

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.yaml is a named bundle of tools a caller is granted as a whole and for a limited time: scopes such as graylog:read, single tools, a description, an optional warning, and a default and maximum duration. Discovery lists the tools a kit covers, marked grant_required, and a denied call names the kits that would allow it.
  • Asking. tell_me_my_rights tells 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_grant asks for a kit with a reason and an optional duration. Both work from execute_code scripts 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. /grants approves, 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-kit and keep running. Denies in config/authz.yaml still 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 in authz.db in the data directory; no OpenFGA service is needed. On a fresh installation the owner and the role admin may call every tool; with users.yaml or apikeys.yaml, plan pro may call every tool and plan free every tool classified read. 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 (preset open). The security-posture summary reports this at every start until you choose a preset (TOOLMESH_AUTHZ_PRESET) or write grants in config/authz.yaml. To begin with the restrictive defaults instead, set TOOLMESH_AUTHZ_MODE=embedded before that first start. OPENFGA_MODE is deprecated; restrict keeps working as TOOLMESH_AUTHZ_MODE=external.
  • A server without a credential rejects every request. Without TOOLMESH_AUTH_PASSWORD, TOOLMESH_API_KEY, users.yaml or apikeys.yaml, requests to /mcp, /files/upload and DELETE /blobs/… get 401. Up to 0.4.1 they were served anonymously.
  • /mcp answers 401 with WWW-Authenticate instead of 200 when 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 gets 503 with Retry-After (on /mcp, /authorize, /files/upload and DELETE /blobs/…).
  • /authorize: a wrong password gets 401 instead of 200, a throttled login 429 with Retry-After, an unregistered client or redirect URI 400 before the password is checked, and a server without a configured password 403.
  • Metrics: toolmesh_logins_total has the new values method="password" and method="anonymous", and its failure series for oauth_bearer and api_key now count rejected bearer credentials.
  • Telemetry carries a site ID, a random UUIDv7 kept in toolmesh.json in the data directory, so the statistics count installations rather than addresses. DO_NOT_SEND_ANONYMOUS_STATISTICS=yes creates no site ID and sends nothing; to discard an existing one, stop ToolMesh and remove site_id from toolmesh.json.
  • New settings: TOOLMESH_AUTHZ_MODE, TOOLMESH_AUTHZ_PRESET, TOOLMESH_AUTHZ_CONFIG, the login limits TOOLMESH_LOGIN_MAX_FAILURES_PER_USER_IP, …_PER_USER, …_PER_IP and TOOLMESH_LOGIN_FAILURE_WINDOW (an invalid value stops the server at startup), TOOLMESH_BCRYPT_MAX_CONCURRENT, and the field key_sha256 in apikeys.yaml.

Upgrade checklist

  1. Keep the data directory writable and persistent; it now holds authz.db, .authz-bootstrap and toolmesh.json.
  2. After the first start, check the log line authorization policy applied and choose a preset or grants if the installation came up as open.
  3. A server that ran without any credential needs TOOLMESH_API_KEY or TOOLMESH_AUTH_PASSWORD.
  4. Add key_sha256 to every entry in apikeys.yaml whose key was generated randomly. The keys stay the same, and key_hash may stay next to it. ToolMesh warns at startup while entries have only a key_hash.
  5. Set TOOLMESH_ISSUER to the public URL clients use. It is now part of the 401 challenge, the protected resource metadata and the approval links, and ToolMesh warns at startup while it is the placeholder.
  6. A reverse proxy must pass 401, 403, 429 and the WWW-Authenticate header on /mcp and /authorize through unchanged.
  7. A login shared by many people, such as a public demo account, should set TOOLMESH_LOGIN_MAX_FAILURES_PER_USER=0 and keep the per-address limits.

Other changes

  • Grants and denies in config/authz.yaml name 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=disabled is 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 DADL access class. discover_tools and toolmesh.describe() now show that access field.
  • The log audit store writes an entry's metadata as a metadata object, as the sqlite store 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

  • /authorize refuses 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.