* feat: add session-scoped connector access for onboarding
* fix(connectors): availability is the config flag AND the portal entitlement — no free-tier leg
The port carried a third availability leg from hermes-magic: a stored guest
(free-tier) identity short-circuits the managed-tool entitlement check. That
leg reads hermes_cli.anon_auth, which does not exist on hermes-agent main, so
connectors_available() raised ImportError inside its fail-closed try and the
whole connector surface was silently dark on a plain upstream checkout.
On this tree availability is the two-leg AND the design started with:
tools.connectors.enabled AND managed_nous_tools_enabled(). The free-tier leg
is a hermes-magic concern and belongs in hermes-magic's own delta over this
branch, next to the identity it depends on. Its integration test goes with it.
* docs(tool-search): connectors section — remote tools through the bridge
The squashed port carried the code but not the user-facing docs. Restores the
Connectors section of the Tool Search page and the connector-gateway host /
CONNECTOR_GATEWAY_URL override on the Tool Gateway page, updated for the
manage_connections tool and the pure-connector batch rule.
* fix(tool-search): connector tools rank with local tools in one pass instead of taking leftover slots
dispatch_tool_search ran BM25 over the local catalog, filled `limit` slots,
then appended connector hits only into slots left empty. On a 300-tool
catalog no slot was ever empty, so with Gmail and Google Calendar connected
"send gmail email" returned five betterstack tools and zero connector tools.
The gateway's hits for a query now become catalog entries (connector name,
slug words, description as the search text) and join the local catalog for
that query's BM25 pass. One ranking, one rarest-token admission rule for both
sources, `limit` as the total per query. The merge loop and the separate
record builder for connector hits are gone; `_shared_tool_record` serves both
sources.
The gateway search timeout rises from 8 s to 30 s. One request with six
use_cases measured 7 s, so 8 s sat on the edge and cut real answers off; the
failure path is unchanged (local-only results, no error to the model).
Live, 311 local tools + gateway, before -> after:
"send gmail email": 5 betterstack tools -> gmail SEND_EMAIL, CREATE_EMAIL_DRAFT
"read google calendar events": 5 betterstack tools -> googlecalendar EVENTS_LIST_ALL_CALENDARS
"linear create issue", "betterstack incident": unchanged
Benchmark (25 labelled queries): connector recall 0.09 -> 0.82, precision@5
0.18 -> 0.59, false positives on absent intents 17 -> 2.
* refactor(tool-search): connector leg into tools/connector_search.py
tools/tool_search.py is a facade. The connector leg (gateway hits as catalog
entries for tool_search, remote schemas for tool_describe, the
connections_in_scope gate) was appended to it by the port. It now lives in
its own sibling, tools/connector_search.py, and the facade imports the three
entry points: connections_in_scope, connector_entries_by_group,
remote_schemas_for.
No behaviour change. The tool_describe remote block became
remote_schemas_for(names, current_tool_defs, connector_describe) with the
same inputs, the same silent-degradation contract and the same injection
seam the tests already use.
* fix(tool-search): at most 7 queries per call, the gateway's search limit
One tool_search call sends all its queries to the connector gateway as one
search request. The gateway answers 7 use_cases per request and returns
HTTP 502 for 8 or more (measured 2026-09-09, re-measured with one-word
use_cases: it is a count limit, not a size limit). With the client cap at
10, a model sending 8 to 10 queries lost every connector hit for that call
and saw local-only results with no error.
The shared constant splits: _MAX_QUERIES_PER_CALL = 7 for search,
_MAX_DESCRIBE_NAMES_PER_CALL = 10 for describe, which has no remote count
limit. Eight or more queries now get the existing "too many queries" retry
hint before any request is made. No chunking: one call, one request.
* fix(tool-search): the model is told that connectors__ names are manage_connections accounts
tool_search results carry names like connectors__gmail__CREATE_EMAIL_DRAFT and
manage_connections is the tool that checks and connects those accounts, but
nothing told the model the two are the same thing. A model that hit
CONNECTION_REQUIRED had to infer the fix on its own.
The tool_search description gains one sentence making the link, added at
assembly only when manage_connections is in the session's tools. Signed out
or with connectors off the tool is absent and the description is unchanged,
so it never names a tool the model cannot call. This follows the existing
rule for cross-tool references (tools/AGENTS.md): they are added dynamically
from the session's actual tool set, never hardcoded in a schema.
Tool defs are fixed for the life of a conversation, so the description is
byte-stable per conversation; this is a one-time prefix change.
Live, real get_tool_definitions() against a signed-in home: sentence present.
Same home with auth.json removed: manage_connections absent, sentence absent.
* fix(connectors): /stop halts a connector batch before the next remote call
dispatch_connector_batch runs every remote entry of a tool_call batch in
sequence. The executor only checks the interrupt flag between tools, and
the whole batch is one tool to it, so a /stop landing during entry 1 of
20 still sent the other 19 to the gateway.
The loop now reads tools.interrupt.is_interrupted before each dispatch.
Once set, it stops calling handle_function_call and fills every unstarted
slot with the loop's existing error-slot shape, code INTERRUPTED and the
message "Stopped by the user before this call was made.", so the result
envelope stays valid and the counts stay honest. Entries already
dispatched keep their real results.
Test: three connector calls where the fake client sets the interrupt on
the first execute. The client sees exactly one call and slots 2 and 3
carry INTERRUPTED. Red on the base branch, green with the fix.
* test(connections): schema assertions become dispatch contracts
test_schema_documents_wait_and_its_timeout froze description fragments
("REQUIRED", "can NOT disconnect", "Nous Portal"). A wording edit fails
it while a real regression (a disconnect that reaches the gateway) does
not. That is a snapshot of prose, not a behaviour contract.
Delete it. The requirement that wait needs connectors is already covered
by test_wait_requires_connectors. The user-only disconnect boundary is
now asserted as behaviour: action disconnect with a connector returns an
error and the fake client records no call. That replaces the earlier
de-authenticate test, which only checked that the word "dashboard"
appeared in the error text.
Test count in the file goes from 26 to 25.
* docs(tool-search): connector batches are one gateway request per entry
The user guide said a connector batch travels as one gateway request. It
does not: model_tools_connectors.dispatch_connector_batch re-enters core
dispatch per entry, and each entry becomes its own execute request in
bridge._run_remote (plus at most one literal-slug retry when the gateway
reports TOOL_NOT_FOUND under the conventional slug). The docstrings in
tools/tool_gateway/bridge.py and tools/tool_gateway/__init__.py still
described the abandoned V1 plan and claimed nothing outside the package
imports it.
Rewrite those sentences to match the code: one request per entry, in
input order, dispatched from model_tools_connectors.py, with the per-entry
approval and interrupt behaviour that motivated the split. The guide also
still showed the single-call shape tool_call(name, arguments); both
places now show the `calls: [{name, arguments}]` array the schema
advertises and note that a single local call is an array of one.
Docs only, no test.
* fix(tools): the between-turns refresh never rewrites the bridge tools
The per-turn MCP refresh folds a fresh tool snapshot into the live array
with preserve_prefix: order and membership stay, but a name present in both
takes the fresh schema. That is right for ordinary tools, whose schema is a
constant. tool_search is the one tool whose description is derived from the
session: the deferred-tool count, the embedded listing, and, on this branch,
whether manage_connections was present. A late MCP server or one failed
portal lookup (manage_connections' check_fn fails closed) changed those bytes
on the next turn, and every byte after tool_search in the cached prefix was
re-prefilled. The array also contradicted itself in that case: the flapping
manage_connections was carried forward while the description lost its hint.
The bridge entries now keep the bytes they were built with for the life of
the conversation. Nothing is lost: tool_search reads the live catalog at
dispatch, so tools that arrived late are still found; connector availability
is checked at dispatch too. The compaction-boundary rebuild (content_aware,
the one sanctioned cache break) still refreshes the description.
Consequence: connector exposure in the prompt is decided once, at agent
build, by whether the user was signed in then. That is the intended
contract.
* refactor(tool-search): normalize_tool_call_entries lives with the other argument validation
The port appended the tool_call argument parser to the tool_search facade.
The family already has tools/tool_search_validation.py for exactly this
work (schema validation of deferred call arguments), so the parser moves
there and the facade imports it. No behaviour change; the one test that
imported it now imports from the defining module.
* refactor(connectors): delete the unused batch dispatcher; _run_remote becomes run_remote
bridge.dispatch_calls and its helpers (_dispatch_calls_inner, _run_pre_dispatch,
_run_local, _error_slot, _maybe_parse_json) and the LocalDispatch / PreDispatch
seams had no production caller. Connector dispatch runs through
model_tools_connectors: dispatch_connector_batch re-enters handle_function_call
once per entry, so scope, hook, approval and middleware policy fire against each
composed name inside core dispatch, and dispatch_connector_call hands the single
planned entry to the bridge's transport function. Only tests called the batch
dispatcher, and they exercised policy seams that production never wires.
The transport function is the module's real entry point, so it drops the
underscore: _run_remote becomes run_remote, body unchanged. The module
docstring now describes the two legs that exist (availability with D32 silent
degradation, and run_remote) instead of the injected seams. Imports that only
the deleted code used are gone; merge.py is untouched because every export
still has a caller.
Tests that drove dispatch_calls are deleted where they covered the removed
seams (pre_dispatch blocks and rewrites, local_dispatch classification, mixed
batches). The literal-slug fallback, the per-entry transport failure, and the
hook rewrite reaching the gateway request body are re-targeted at
handle_function_call('tool_call', ...) with the fake client swapped in at
bridge._default_client_factory, the same seam test_connector_dispatch_policy
uses. Each re-targeted test fails when the retry is disabled in run_remote.
* fix(connectors): search keeps the twin a colliding name reaches, and says so
format_connector_name strips the toolkit prefix, so GMAIL_FETCH_PROFILE and a
literal FETCH_PROFILE on gmail both compose to connectors__gmail__FETCH_PROFILE.
describe and execute decode that name to the prefixed slug first, so the
literal twin is unreachable under it. If a vendor ever shipped both, search
could describe the literal under a name that runs the prefixed tool.
Search is the one place that sees both twins in one response. It now keeps
the twin the name reaches and drops the other with a WARNING that names both
slugs, whichever the gateway listed first. Short names stay; no marker, no
per-process map, no change to describe or execute. No such pair exists in the
live catalog today; the guard turns a silent alias into a logged one.
13 KiB
title, description, sidebar_label, sidebar_position
| title | description | sidebar_label | sidebar_position |
|---|---|---|---|
| Nous Tool Gateway | One subscription, every tool. Web search, image generation, TTS, and cloud browsers — all routed through Nous Portal with no extra API keys. | Tool Gateway | 2 |
Nous Tool Gateway
One subscription. Every tool built in.
The Tool Gateway is included with every paid Nous Portal subscription. It routes Hermes' tool calls — web search, image generation, text-to-speech, and cloud browser automation — through infrastructure Nous already runs, so you don't have to sign up with Firecrawl, FAL, OpenAI, Browser Use, or anyone else just to make your agent useful.
What's included
| Tool | What you get | |
|---|---|---|
| 🔍 | Web search & extract | Agent-grade web search and full-page extraction via Firecrawl. No rate limits to worry about — the gateway handles scaling. |
| 🎨 | Image generation | Nine models under one endpoint: FLUX 2 Klein 9B, FLUX 2 Pro, Z-Image Turbo, Nano Banana Pro (Gemini 3 Pro Image), GPT Image 1.5, GPT Image 2, Ideogram V3, Recraft V4 Pro, Qwen Image. Pick per-generation with a flag, or let Hermes default to FLUX 2 Klein. |
| 🔊 | Text-to-speech | OpenAI TTS voices wired into the text_to_speech tool. Drop voice notes into Telegram, generate audio for pipelines, narrate anything. |
| 🌐 | Cloud browser automation | Headless Chromium sessions via Browser Use. browser_navigate, browser_click, browser_type, browser_vision — all the agent-driving primitives, no Browserbase account required. |
All four are pay-as-you-use billed against your Nous subscription. Use any combination — run the gateway for web and images while keeping your own ElevenLabs key for TTS, or route everything through Nous.
Why it's here
Building an agent that can actually do things means stitching together 5+ API subscriptions — each with their own signup, rate limits, billing, and quirks. The gateway collapses that into one account:
- One bill. Pay Nous; we handle the rest.
- One signup. No Firecrawl, FAL, Browser Use, or OpenAI audio accounts to manage.
- One key. Your Nous Portal OAuth covers every tool.
- Same quality. Same backends the direct-key route uses — just fronted by us.
Bring your own keys anytime — per-tool, whenever you want to. The gateway isn't a lock-in, it's a shortcut.
Get started
There are three ways in — pick whichever fits where you are:
hermes setup --portal # Fresh install: Nous OAuth + set Nous as provider + turn on the Tool Gateway in one go
hermes model # Switch your inference provider to Nous Portal — Hermes then offers to turn on the gateway for all tools
hermes tools # Enable the gateway per-tool — pick "Nous Subscription" for any tool you want
hermes setup --portal and hermes model are the all-at-once paths: log in once, optionally flip every tool to the gateway. hermes tools is the à la carte path — turn on just the tools you want, one at a time.
You don't have to log in first. With hermes tools, the Nous-managed backends (Web search, Image, Video, TTS, Browser) are always listed, even if you've never signed into Nous Portal. Select one and Hermes runs the Portal login right there if you aren't already authenticated — no need to run hermes model beforehand. If your Nous OAuth is already active, selecting the backend enables it immediately with no extra prompt. This path only logs you in and turns on the one tool you picked — it does not switch your inference provider, and it does not prompt you to enable the gateway for every other tool.
Check what's active at any time:
hermes portal info # Portal auth + Tool Gateway routing summary
hermes portal tools # Gateway catalog with current routing per tool
hermes status # Full system status (Tool Gateway is one section)
hermes portal info shows a section like:
◆ Nous Tool Gateway
Nous Portal ✓ managed tools available
Web tools ✓ active via Nous subscription
Image gen ✓ active via Nous subscription
TTS ✓ active via Nous subscription
Browser ○ active via Browser Use key
Tools marked "active via Nous subscription" are going through the gateway. Anything else is using your own keys.
Eligibility
The Tool Gateway is a paid-subscription feature. Free-tier Nous accounts can use Portal for inference but don't include managed tools — upgrade your plan to unlock the gateway.
Some accounts are also entitled to a free tool pool — a small managed-tool allowance that covers gateway tool calls without a paid subscription. When a free pool is available, the gateway surfaces it and shows a setup prompt on first use, so you can opt in and start using managed tools right away.
The enablement checklist
Picking a Nous model (hermes model) offers a per-tool checklist of gateway backends. Its behavior respects your existing setup:
- Tools you've explicitly pointed at another backend (e.g.
web.backend: searxng,browser.cloud_provider: camofox) are never offered — your selection can't be accidentally overwritten. - Tools configured via environment variables alone (e.g.
SEARXNG_URL,CAMOFOX_URL) are offered unchecked, labeled to keep your own backend. - Only genuinely unconfigured tools come pre-checked.
- Declines stick: if you submit the checklist with a tool unchecked, it won't be pre-checked on future Nous model swaps (stored in
tool_gateway_declined_toolsinconfig.yaml; checking it later clears the decline).
Mix and match
The gateway is per-tool. Turn it on for just what you want:
- All tools through Nous — easiest; one subscription, done.
- Gateway for web + images, bring your own TTS — keep your ElevenLabs voice, let Nous handle the rest.
- Gateway only for things you don't have keys for — "I already pay for Browserbase, but I don't want a Firecrawl account" works fine.
Switch any tool at any time via:
hermes tools # Interactive picker for each tool category
Select the tool, pick Nous Subscription as the provider (or any direct provider you prefer). No config editing required. If you aren't logged into Nous Portal yet, picking Nous Subscription kicks off the Portal login inline — you don't need to authenticate through hermes model first.
Using individual image models
Image generation defaults to FLUX 2 Klein 9B for speed. Override per-call by passing the model ID to the image_generate tool:
| Model | ID | Best for |
|---|---|---|
| FLUX 2 Klein 9B | fal-ai/flux-2/klein/9b |
Fast, good default |
| FLUX 2 Pro | fal-ai/flux-2-pro |
Higher fidelity FLUX |
| Z-Image Turbo | fal-ai/z-image/turbo |
Stylized, fast |
| Nano Banana Pro | fal-ai/nano-banana-pro |
Google Gemini 3 Pro Image |
| GPT Image 1.5 | fal-ai/gpt-image-1.5 |
OpenAI image gen, text+image |
| GPT Image 2 | fal-ai/gpt-image-2 |
OpenAI latest |
| Ideogram V3 | fal-ai/ideogram/v3 |
Strong prompt adherence + typography |
| Recraft V4 Pro | fal-ai/recraft/v4/pro/text-to-image |
Vector-style, graphic design |
| Qwen Image | fal-ai/qwen-image |
Alibaba multimodal |
The set evolves — hermes tools → Image Generation shows the current live list.
Configuration reference
Most users never need to touch this — hermes model and hermes tools cover every workflow interactively. This section is for writing config.yaml directly or scripting setups.
One selection key per tool category
Each tool category has a single provider-selection key, written by the hermes tools picker (or the desktop GUI). Picking the Nous Subscription row stores the value nous, which routes that category through the managed Tool Gateway. Picking a BYOK row stores the vendor name (fal, openai, firecrawl, browser-use, ...), which goes direct with your own credentials:
web:
backend: nous # web search/extract via the Tool Gateway
image_gen:
provider: nous # image generation via the Tool Gateway
tts:
provider: nous # TTS via the Tool Gateway
stt:
provider: nous # speech-to-text via the Tool Gateway
browser:
cloud_provider: nous # cloud browser via the Tool Gateway
The runtime always uses the stored selection — credential presence never selects or reroutes a category. A FAL_KEY sitting in .env is ignored while image_gen.provider: nous; conversely, image_gen.provider: fal with no FAL_KEY set produces a clear error instead of silently falling back to the gateway:
image_gen is configured to use fal (set via hermes tools), but FAL_KEY is not set. Run 'hermes tools' to change it.
Categories you have never configured (no selection key ever written) autodetect from available credentials, same as before. But once a selection exists, adding a key to .env does not change the route — only hermes tools (or editing the selection key) does.
Switching back to your own keys
hermes tools # pick the tool → choose a direct provider (e.g. Firecrawl)
Or set the selection key directly:
web:
backend: firecrawl # Hermes now uses FIRECRAWL_API_KEY from .env
Legacy use_gateway flag (deprecated)
Older Hermes versions used a per-tool use_gateway: true boolean to route through the gateway. That flag is legacy: it is never written anymore, and the hermes tools picker removes it from a category's config when it rewrites the selection. Old configs that still contain use_gateway: true are interpreted at read time as the nous selection, so existing setups keep working. Don't set use_gateway in new configs — select the provider in hermes tools instead.
Self-hosted gateway (advanced)
Running your own Nous-compatible gateway? Override endpoints in ~/.hermes/.env:
TOOL_GATEWAY_DOMAIN=your-domain.example.com
TOOL_GATEWAY_SCHEME=https
TOOL_GATEWAY_USER_TOKEN=your-token # normally auto-populated from Portal login
FIRECRAWL_GATEWAY_URL=https://... # override one endpoint specifically
TOOL_GATEWAY_URL=http://127.0.0.1:3009 # pin the shared managed origin exactly
CONNECTOR_GATEWAY_URL=http://127.0.0.1:3009 # pin the connectors origin exactly
Every host is named {label}-gateway.<domain>, and TOOL_GATEWAY_DOMAIN / TOOL_GATEWAY_SCHEME reshape all of them; a {LABEL}_GATEWAY_URL pins one host exactly and skips the derivation:
{vendor}-gateway.<domain>— per-vendor passthroughs (Firecrawl, BFL, ...).tool-gateway.<domain>— the shared managed origin: the vendors hosted on the gateway itself plus media uploads.connector-gateway.<domain>— the connectors API (/v1/connectors/*), its own deployment. See Tool Search → Connectors.
These knobs exist for custom infrastructure setups (enterprise deployments, dev environments). Regular subscribers never set them.
FAQ
Does it work with Telegram / Discord / the other messaging gateways?
Yes. Tool Gateway operates at the tool-execution layer, not the CLI. Every interface that can call a tool — CLI, Telegram, Discord, Slack, IRC, Teams, the API server, anything — benefits from it transparently.
What happens if my subscription expires?
Tools routed through the gateway stop working until you renew or swap in direct API keys via hermes tools. Hermes shows a clear error pointing at the portal.
Can I see usage or costs per tool?
Yes — the Nous Portal dashboard breaks usage down by tool so you can see what's driving your bill.
Is Modal (serverless terminal) included?
Modal is available as an optional add-on through the Nous subscription, not part of the default Tool Gateway bundle. Configure it via hermes setup terminal or directly in config.yaml when you want a remote sandbox for shell execution.
Do I need to delete my existing API keys when I enable the gateway?
No — keep them in .env. While a tool's selection is Nous Subscription, direct keys for that tool are simply ignored. Pick the direct provider again in hermes tools and your keys become the source again. The gateway isn't a lock-in.