Skip to main content

Agent and Tool Configuration

Every field that decides what an agent is and what it may use. For what these capabilities do, read Agents, Knowledge and Data Connectors.

This page is one of four. The configuration reference was a single 2,929-line document. Its own second section was a hand-written table of contents — a page admitting it could not be navigated. It is now split by subject, so the fields for a thing sit beside the pages that explain the thing:

PageHolds
Configurationhow config is loaded, secrets, environment variables, channels, and the deployment-wide settings
Provider and Model Configurationevery provider, model preset, fallback and transcription field
Agent and Tool Configurationnamed agents, tool groups, web tools, MCP, knowledge and connectors
Security Configurationcapability gates, approvers, standing grants and pairing

Examples are snippets to merge into ~/.nanoinfra/config.json, not replacement files. The docs use camelCase because nanoinfra writes config that way.

Named Agents​

One agent answers by default. agents.named names more of them, each with its own model, its own tools, and its own instructions.

{
"agents": {
"named": {
"sre-prod": {
"description": "Hands-on checks on production hosts",
"modelPreset": "kimi-general",
"toolGroups": ["servers"],
"skills": ["servers"],
"addendum": "Prefer read-only checks."
},
"manager": {
"description": "Plans work and asks the right specialist",
"delegates": ["sre-prod"]
}
}
}
}
KeyTypeDefaultMeaning
descriptionstring""What the agent is for. Reaches the model when a peer decides whom to ask, and the picker in the WebUI.
modelPresetstringinheritsWhich model preset this agent answers with.
toolGroupsarray | nullabsent = every groupWhich tool groups this agent may use. Naming groups narrows. It never widens. [] means none, not all.
skillsarray | nullabsent = the catalogue summarySkills loaded in full for this agent. [] loads none and summarises no catalogue either.
connectorsarray | nullabsent = whatever config activatesData connectors this agent may reach. [] means none.
mcpServersarray | nullabsent = whatever config activatesMCP servers this agent may reach. [] means none — which is how a deployment stops paying for every installed server on every turn.
addendumstring""Appended after the platform's prompt sections. Cannot replace the tool contract or the safety notes.
promptSectionsobject{}Replaces named prompt sections outright, rather than appending like addendum. Which sections may be replaced is decided in nanoinfra/agent/prompt_sections.py, not by this schema: naming a fixed one is refused. A replacement is recorded in the prompt manifest, so two different prompts never look identical.
delegatesarray[]The peers this agent may ask. Membership is the grant.

Deployment-wide agent keys​

These sit on agents.defaults and every named agent inherits them.

KeyDefaultMeaning
promptSections{}The same section replacement as above, for the default agent.
maxToolIterations200How many provider/tool round trips one turn may make before the runner stops and finalises. Raise it for long tool chains. A turn that hits the ceiling still answers rather than failing. Distinct from the my tool's runtime max_iterations, which a turn sets for itself and which is capped at 100 — see My Tool.
consolidationRatio0.5What fraction of the session Dream folds into memory when it consolidates. Bounded to 0.1–0.95, because consolidating everything leaves no recent history and consolidating almost nothing never reclaims context.
botName"nanoinfra"The display name in CLI prompts, as in {name} is thinking….
botIcon"🐈"A short icon shown beside the name in the CLI. Set "" to omit it.

An empty named block is the shape every deployment has today, and nothing about the WebUI or the prompt changes until you fill it.

Two things are refused when config loads, rather than at the moment they would fail. The first is a delegates entry naming an agent that does not exist. The second is a name that could not be typed as @agent:<name>.

See Agents for what a manager does with a roster, who signs a delegated action, and how an automation uses a narrower agent.

Built-in tool groups​

Every built-in tool's schema is in every prompt, on every turn. On the demo that was measured: a greeting cost 17,302 tokens, and the 31 built-in schemas were 10,273 of them. Two clusters accounted for 3,857 — 22% of the whole prompt — for capabilities the turn never touched:

grouptoolstokens
diagramscreate_diagram update_diagram get_diagram list_diagrams list_diagram_components2,438
serverscreate_server update_server delete_server get_server list_servers execute_on_server1,419

A group in attach: "mention" mode contributes one advertised line instead, and its schemas only for a turn that names it:

{
"tools": {
"groups": {
"diagrams": { "attach": "mention" },
"servers": { "attach": "mention" }
}
}
}

diagrams and servers are defined by nanoinfra, so config carries the mode and not the tool names. Both default to always, which is what every deployment did before this option existed. A tool that disappeared because a release regrouped it would be a behaviour change nobody asked for. Turning this on is deliberate.

A group can also be your own:

{
"tools": {
"groups": {
"reporting": {
"attach": "mention",
"description": "generate the weekly report",
"tools": ["read_file", "mcp_sheets_append_row"]
}
}
}
}

Any registered tool name works, including an MCP or connector tool. A tool listed in an always group as well as a mention one stays available. Both are explicit statements, and a capability silently withdrawn is the worse failure.

Naming a group​

Say @diagrams anywhere in the message. Unlike an MCP server or a connector mention, this is read from the message text. So it works from Telegram, Discord and the CLI as well as the WebUI. The advertised line tells the user to say @diagrams, so it works wherever they can say it.

An automation types no @, so a cron job or a trigger declares its groups instead:

{ "toolGroups": ["diagrams"] }

What the model is told​

One line per withheld group, inside the stable prompt block:

# Available tool groups

These built-in tools are installed and their schemas are **not** in this prompt. You cannot call
them until the user names the group. If a request needs one, say which and ask the user to attach
it -- do not substitute a different tool.

- `diagrams` — read, create and update saved infrastructure diagrams: 5 tools, not loaded. Say
`@diagrams` to use them this turn.

Roughly 50 tokens against the 2,438 the schemas cost, and that is what makes the trade acceptable. A model that cannot see that a capability exists cannot say "I can do that if you attach it". It fails, or quietly substitutes something worse — and a silently worse answer is harder to notice than a large bill. A group whose tools are all absent from this deployment is not advertised at all. Telling somebody to say @diagrams when the attachment would load nothing is worse than silence.

The cost to accept, stated plainly. A turn that needed a diagram and did not name the group gets a worse answer than it would have. The advertised line is what lets the agent say so instead of guessing.

Letting the model search for a group​

mention waits for a person. attach: "search" is the model-driven counterpart. The schemas are withheld the same way, but the model loads a group itself by calling the built-in tool_search tool with a topic. There is no @group to type.

{
"tools": {
"groups": {
"diagrams": { "attach": "search" }
}
}
}

The difference that matters is cost as you defer more. mention pays one advertised line per group. search replaces the whole enumeration with a single pointer, regardless of how many groups are withheld:

# Searchable tools

Some installed tools are not loaded into this prompt. If a request needs a capability you have no
tool for, call `tool_search` with the topic to load the matching tools for this turn, then use
them.

So mention is the mode for a capability a person should authorise per turn. search is the mode that scales when you want to defer many groups, without the advertised block itself becoming the cost. Both hide the schemas, and both respect an agent's toolGroups ceiling. A group outside the acting agent's ceiling is never a search result, and it is refused if named anyway. tool_search appears at all only when some group, MCP server or connector is set to search. The same attach: "search" mode is available on MCP servers and connectors, searched by the one tool_search tool.

MCP (Model Context Protocol)​

[!TIP] The config format is compatible with Claude Desktop / Cursor. You can copy MCP server configs directly from any MCP server's README.

nanoinfra supports MCP — connect external tool servers and use them as native agent tools.

Add MCP servers to your config.json:

{
"tools": {
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"]
},
"my-remote-mcp": {
"url": "https://example.com/mcp/",
"headers": {
"Authorization": "Bearer xxxxx"
}
}
}
}
}

Two transport modes are supported:

ModeConfigExample
Stdiocommand + args, optional cwdLocal process via npx / uvx
HTTPurl + headers (optional)Remote endpoint (https://mcp.example.com/sse)

cwd sets the working directory a stdio server runs in, which is where it writes its own runtime artifacts. It defaults to "", meaning the server inherits the MCP host's directory. Set it when a server expects to find or create files beside itself.

[!IMPORTANT] HTTP/SSE MCP URLs are validated before probing or connecting, and every outgoing MCP HTTP request is validated again before redirects are followed. localhost, 127.0.0.1, RFC1918/private IPs, CGNAT/Tailscale ranges, link-local addresses, and cloud metadata endpoints are blocked by default. This can break previously working local or private HTTP MCP configs. The fix is to allow the endpoint explicitly with tools.ssrfWhitelist, preferably with a single-host CIDR such as 127.0.0.1/32, ::1/128, or 192.168.1.50/32. Stdio MCP servers are not affected.

Use toolTimeout to override the default 30s per-call timeout for slow servers:

{
"tools": {
"mcpServers": {
"my-slow-server": {
"url": "https://example.com/mcp/",
"toolTimeout": 120
}
}
}
}

Use enabledTools to register only a subset of tools from an MCP server:

{
"tools": {
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"],
"enabledTools": ["read_file", "mcp_filesystem_write_file"]
}
}
}
}

enabledTools accepts either the raw MCP tool name (for example read_file) or the wrapped nanoinfra tool name (for example mcp_filesystem_write_file).

  • Omit enabledTools, or set it to ["*"], to register all capabilities (tools, resources, and prompts).
  • Set enabledTools to [] to register no tools from that server. Resources and prompts are also skipped, since they have no per-name filter.
  • Set enabledTools to a non-empty list of names to register only those tools — resources and prompts are not registered.

MCP tools are automatically discovered and registered on startup. The LLM can use them alongside built-in tools — no extra configuration needed.

Web Tools​

nanoinfra incorporates basic tools for accessing the web. These include searching via APIs, and fetching arbitrary web pages in Markdown format. They are enabled by default, and can be configured in ~/.nanoinfra/config.json under tools.web.

If you want to disable them, which removes both web_search and web_fetch from the tool list sent to the LLM, set tools.web.enable to false:

{
"tools": {
"web": {
"enable": false
}
}
}

nanoinfra uses a shared SSRF guard for built-in web fetches and HTTP/SSE MCP connections. By default it blocks loopback, RFC1918/private ranges, CGNAT/Tailscale ranges, link-local addresses, and cloud metadata endpoints. If you need to allow trusted private ranges, explicitly exempt them from SSRF blocking with tools.ssrfWhitelist:

{
"tools": {
"ssrfWhitelist": ["100.64.0.0/10"]
}
}

Keep whitelist entries as narrow as possible, such as a single host CIDR (192.168.1.50/32). The whitelist is global for the shared SSRF guard. It is not limited to one tool or one MCP server.

HTTP/SSE MCP connections use the same process-wide proxy environment behavior as web_fetch. Proxied targets use the configured proxy. URLs excluded by NO_PROXY remain DNS-pinned direct connections.

[!TIP] Use proxy in tools.web to route web requests through a proxy:

{ "tools": { "web": { "proxy": "http://127.0.0.1:7890" } } }

web_fetch applies DNS pinning for direct connections. An explicit tools.web.proxy, or a process-wide proxy environment variable, can apply to the target URL. nanoinfra then still validates the requested URL locally, but DNS resolution for the outbound fetch happens at the proxy. Configure only trusted proxies. URLs excluded by NO_PROXY keep the DNS-pinned direct path unless tools.web.proxy is configured.

tools.web​

OptionTypeDefaultDescription
enablebooleantrueEnable or disable all built-in web tools (web_search + web_fetch)
proxystring or nullnullProxy for web requests, for example http://127.0.0.1:7890. web_fetch DNS pinning applies only to direct connections. Proxied fetches rely on the configured proxy as the trusted network exit.
userAgentstring or nullnullUser-Agent header for all web requests. If null, a browser one will be used

nanoinfra supports multiple web search providers. Configure in ~/.nanoinfra/config.json under tools.web.search.

By default, web search uses duckduckgo, and it works out of the box without an API key.

ProviderConfig fieldsEnv var fallbackFree
braveapiKeyBRAVE_API_KEYNo
tavilyapiKeyTAVILY_API_KEYNo
jinaapiKeyJINA_API_KEYFree tier (10M tokens)
kagiapiKeyKAGI_API_KEYNo
olostepapiKeyOLOSTEP_API_KEYNo
bochaapiKeyBOCHA_API_KEYFree tier (1M calls for startups)
volcengineapiKeyVOLCENGINE_SEARCH_API_KEY or WEB_SEARCH_API_KEYMonthly quota, then paid
keenableapiKey (optional)KEENABLE_API_KEYYes (no key needed. Key raises limits)
searxngbaseUrlSEARXNG_BASE_URLYes (self-hosted)
duckduckgo (default)——Yes

Brave:

{
"tools": {
"web": {
"search": {
"provider": "brave",
"apiKey": "${BRAVE_API_KEY}"
}
}
}
}

Tavily:

{
"tools": {
"web": {
"search": {
"provider": "tavily",
"apiKey": "${TAVILY_API_KEY}"
}
}
}
}

Jina (free tier with 10M tokens):

{
"tools": {
"web": {
"search": {
"provider": "jina",
"apiKey": "${JINA_API_KEY}"
}
}
}
}

Kagi:

{
"tools": {
"web": {
"search": {
"provider": "kagi",
"apiKey": "${KAGI_API_KEY}"
}
}
}
}

Olostep:

{
"tools": {
"web": {
"search": {
"provider": "olostep",
"apiKey": "${OLOSTEP_API_KEY}"
}
}
}
}

You can also set OLOSTEP_API_KEY in the environment instead of storing it in config.

Bocha (AI-optimized search, free tier available):

{
"tools": {
"web": {
"search": {
"provider": "bocha",
"apiKey": "${BOCHA_API_KEY}"
}
}
}
}

Create your API key at open.bochaai.com. Bocha returns structured results optimized for AI consumption, with optional summaries. You can set BOCHA_API_KEY in the environment instead of storing it in config.

Volcengine Search:

{
"tools": {
"web": {
"search": {
"provider": "volcengine",
"apiKey": "${VOLCENGINE_SEARCH_API_KEY}"
}
}
}
}

You can also set WEB_SEARCH_API_KEY for compatibility with the Volcengine web-search skill. Create the key in the Volcengine web search console, then copy it from API keys. Volcengine Ark keys are separate and do not work for this search provider.

Keenable (works without an API key on the free tier):

{
"tools": {
"web": {
"search": {
"provider": "keenable"
}
}
}
}

Keenable search works out of the box with no account, via its token-less public endpoint (free tier, limited to 1,000 requests/hour). Set apiKey (or KEENABLE_API_KEY) from keenable.ai to remove the hourly limit.

Serper (Google Search API):

{
"tools": {
"web": {
"search": {
"provider": "serper",
"apiKey": "${SERPER_API_KEY}"
}
}
}
}

Create a key at serper.dev. You can also set SERPER_API_KEY in the environment instead of storing it in config.

SearXNG (self-hosted, no API key needed):

{
"tools": {
"web": {
"search": {
"provider": "searxng",
"baseUrl": "https://searx.example"
}
}
}
}

DuckDuckGo (zero config):

{
"tools": {
"web": {
"search": {
"provider": "duckduckgo"
}
}
}
}

tools.web.search​

OptionTypeDefaultDescription
providerstring"duckduckgo"Search backend: brave, tavily, jina, kagi, olostep, bocha, volcengine, keenable, serper, searxng, duckduckgo
apiKeystring""API key for API-backed search providers
baseUrlstring""Base URL for SearXNG
maxResultsinteger5Results per search (1–10)

Web Fetch​

[!TIP] If you are having issues with JS proof-of-work or Cloudflare captchas, set a random user agent and disable Jina Reader:

{ "tools": { "web": { "userAgent": "Not-A-Browser", "fetch": { "useJinaReader": false } } } }

nanoinfra by default uses Jina Reader, a third-party API, to convert arbitrary pages into Markdown format for easy digestion by the LLM. If Jina Reader fails, nanoinfra uses a local conversion based on readability-lxml instead.

If you want to always use the local conversion, you can force it using:

{
"tools": {
"web": {
"fetch": {
"useJinaReader": false
}
}
}
}

tools.web.fetch​

OptionTypeDefaultDescription
useJinaReaderbooleantrueIf true, Jina Reader will be preferred over the local conversion

CLI Apps​

tools.cliApps holds the runtime limits for CLI Apps — the packaged command-line programs the agent can install and run. The catalogue itself is managed in Apps in the WebUI. These are the three timeouts behind it.

KeyDefaultMeaning
installTimeout300Seconds one install may take before it is abandoned. Installs fetch packages, so this is the longest of the three.
runTimeout60Seconds one CLI App invocation may run.
catalogTtlSeconds3600How long a fetched catalogue is reused before it is fetched again. A shorter value means more requests to the catalogue source. A longer one means a newly published app takes longer to appear.
{
"tools": {
"cliApps": {
"installTimeout": 300,
"runTimeout": 60,
"catalogTtlSeconds": 3600
}
}
}

Knowledge Base​

tools.knowledge turns on knowledge_search and the knowledge-index automation. Documents live in <workspace>/knowledge/ and the index sits beside them. Nothing is injected into a prompt, so a knowledge base costs nothing on a turn that does not ask for it.

{
"tools": {
"knowledge": {
"enabled": true,
"mode": "lexical",
"reindexIntervalS": 900,
"exclude": [".env", ".env.*", "*.pem", "*.key", "id_*", "secrets/**", "**/.git/**"],
"maxFileBytes": 2000000,
"maxTotalBytes": 200000000,
"maxResults": 5
}
}
}
OptionTypeDefaultDescription
enabledbooleanfalseRegisters the search tool and the indexing job. Needs a gateway restart.
modestringlexicallexical (BM25F, no extra dependency) or hybrid (needs pip install 'semlix[semantic]').
reindexIntervalSinteger900How often the full pass runs. Minimum 60. The search tool indexes what changed on its own, so this can stay quiet.
excludestring[]secretsGlobs never indexed. Matched against the file name and against the path at any depth, so secrets/** also excludes nested/secrets/keys.md.
maxFileBytesinteger2000000Largest document. A larger one is skipped and reported, never silently dropped.
maxTotalBytesinteger200000000Total indexed size.
maxResultsinteger5Fragments one search returns (1-25).

See Knowledge for the citation contract, what each mode costs, and why a document is not being found.

Data Connectors​

The connectors block holds the data sources this deployment reaches, and the OAuth credentials they name. For the model behind these keys, and the setup walk-through, read data-connectors.md first.

An absent block activates nothing. A connector is installed code with no token and no capability class until config names it, which is the same rule tools.agentPlugins states.

{
"connectors": {
"credentials": {
"google_calendar_credential": {
"clientId": "1234-abc.apps.googleusercontent.com",
"secretRef": "3f2a…",
"clientSecretRef": "9c41…",
"scopes": [
"https://www.googleapis.com/auth/calendar.readonly",
"https://www.googleapis.com/auth/calendar.events"
]
}
},
"connectors": {
"google-calendar": {
"credential": "google_calendar_credential",
"enabledOperations": ["list_events", "get_event"],
"maxClass": "read",
"settings": { "calendarId": "primary" }
}
},
"active": ["google-calendar"]
}
}

Credential Keys​

OptionDefaultDescription
kind"oauth2"The credential type. oauth2 is the only value today.
clientId""The OAuth client id for this deployment. One client per deployment: a shared id shares a quota, a verification status, and one revocation.
secretRef""The id of the secret holding the refresh token. A reference, never a value — a config file has more readers than a secret store.
clientSecretRef""The id of the secret holding the OAuth client secret. Google requires it in the refresh exchange even for a desktop client.
tokenUrl""Left empty, the connector's own manifest supplies it. Set it only for a provider whose endpoint differs.
scopes[]What the consent actually granted. The refresh exchange asks for the intersection of this and what the connector declared for the class in play.

nanoinfra connectors authorize <name> writes both secrets and prints this block with the real ids.

Connector Keys​

OptionDefaultDescription
credential""Which entry of connectors.credentials this connector may resolve. This binding is the grant — there is no second allow-list, so one OAuth flow can serve several connectors.
enabledOperationsnullThe operations that reach the model. null means every operation the manifest declares. Naming an operation the package lacks fails activation.
maxClassnullA ceiling on the classes this connector may offer. "read" leaves it no writes however its manifest is written. Accepts read or mutate.remote.
settings{}The connector's own fields, validated by its own package. For google-calendar: calendarId, which defaults each call.
connectors.active[]The connectors this deployment activates, named by their package directory. A connector absent from this list contributes no tools.

How the Gate Reads This Block​

  • Every object inside connectors refuses an unknown key. A mistyped enabledOperatons fails to load rather than becoming an absent restriction.
  • Activation resolves at start. A connector does not activate if its credential is missing a scope. The same is true if enabledOperations names something the package lacks, or if its ceiling leaves nothing to call. The boot log names the key that fixes it.
  • The executor re-reads this block for each call, so tightening enabledOperations or maxClass applies without a restart. Adding a connector to active needs one, because the tools are registered when the agent starts.
  • No connector reads a secret in the gateway. The refresh token is exchanged in the executor process, and the agent receives no token at all.

Image Generation​

Image generation is configured under tools.imageGeneration and uses credentials from the selected provider's providers.<name> block.

See Image Generation for WebUI usage, provider examples, artifact storage, and troubleshooting.