The tools config
An agent’s tools are declared as one tools object: a default_config that sets the baseline for every tool, plus a configs map of per-tool overrides keyed by tool name. This is how you enable or disable individual tools and set each one’s permission.
default_configapplies to every tool; aconfigsentry overrides one tool by name.permissionis one ofallow(run silently),ask(pause →requires_action), ordeny(the tool is not offered to the model at all).- Common patterns: enable-only (
default_config.enabled: false, then enable individually) and trust-by-default-except (allow all, set one tool toask). - Running sessions keep the config they started with; edits apply to new sessions. Tools can also be re-selected per session while it is idle.
Built-in tools
ask_operator is the one tool with no allow. The tool is the pause, so allow collapses to ask: the session stops, reports awaiting_answer, and the question waits in pending_actions until someone answers it — see the awaiting-answer loop. Set deny (or enabled: false) to forbid an agent to ask at all. It is not offered on a harness that cannot hold a call open for a person, and a session that enables it on one is refused when it starts.The last five are a team’s tools: an agent with no roster is never offered one, a teammate is never offered
send_to_agent — work is delegated one level, never onward — and the two board tools appear only for a team that declared a board. They are listed here because a tool you cannot name is a tool you cannot deny.Every other tool above is on by default. A tool the session has no computer for is simply not offered — a text-only session gets none of the six sandbox tools (bash, read, write, edit, ls, find) and keeps the rest. The one exception is read: a text-only session that pinned skills still gets it, because a skill’s body is a file the model is told the location of, and a location nothing can open would be a lie. It reads the skills and nothing else — there is no filesystem behind it. There is deliberately no grep: its search runs a local process the sandbox cannot redirect, so it would search the wrong machine. bash covers the capability against the right filesystem.Web tools
web_search and web_fetch reach the open web, so each one takes its own filters and its own cap
on how much page text may enter the context. Both are configured in the same configs map as every
other tool:
allowed_domains— a non-empty list is a closed list: nothing outside it is reachable. Empty or absent means the open web. A domain covers its subdomains, soexample.commatchesdocs.example.comand nevernotexample.com.blocked_domains— always wins overallowed_domains, so one entry carves a hole in a broad allow-list and the answer never depends on list order.max_content_tokens— how much of a fetched page may enter the context; the rest is truncated. Defaults to4000. Context you never load is context you never pay for.- The two tools are configured separately — searching broadly while fetching only from a short list of trusted hosts is the common shape.
- A search result outside the policy is dropped from the results; a
web_fetchoutside it is refused before the page is requested. A page that redirects off the policy is refused too — the host that actually answered is the one that has to be admitted, so an open redirect on an allowed host cannot be used to reach past the list. That refusal comes after the fetch, so unlike the others it is billed. - Both tools are priced per call against the budget and appear on
GET /v1/agents/{id}/spend?by=componentundersearch. The price is what the call cost, so it is booked after the call rather than quoted before it — a domain filter bounds where an agent can go,cap_micro_usdbounds how much it spends getting there.
Generation tools
Both save what they produce to Files and hand the agent afile_id; they
differ in when.
generate_image answers in the same call. An image renders in seconds, so the tool waits, saves the
result and returns its file_id there and then.
generate_video does not block. A render measured in minutes cannot happen inside a turn, so
the tool starts the job and returns a handle straight away and the agent carries on working. When
the job finishes, the file is saved and the session is woken and told its file_id. Nothing is
polled and nothing is waited on, so a long render never consumes a turn.
models— which models this agent may generate with, out of the live catalogue atGET /v1/media/models. It narrows; there is no fixed list to choose from and no enum, because the catalogue belongs to the provider and changes without a release of ours. Omit it and the agent may use anything the provider publishes. Name models and the agent is held to them, with the first one you name as its default.- Choosing a model. With no
modelspinned,generate_imageruns the cheapest model the provider publishes a price for, andgenerate_videoasks the agent to name one — video carries no published price, so there is no cheapest to fall back to and we will not pick a favourite for you. A refusal lists ids the agent can actually use. Asking for a model outside the pin, or one the catalogue does not have, is refused before anything is submitted. - Before a job starts, the room left under the agent’s
max_task_usd(budgets) is held. A generation job’s cost cannot be known until it finishes, so what the reservation does is admit the job rather than price it: an agent with no real headroom left is refused and nothing is spent. The check happens before the render, not after it. - You are billed for what the finished job actually cost, once per job, and the rest of the
reservation is released the moment the job settles. It appears on
GET /v1/agents/{id}/spend?by=componentundermedia. - A job that fails, or that never finishes, tells the session so and stores nothing.
seconds,aspect_ratioandseedare optional; a model clamps them to what it supports.
Permission policies
Every server-executed tool resolves toallow, ask, or deny before it runs — set a default in default_config and override per tool in configs, and respond to ask with a tool.confirm event. Full detail, including the confirmation flow and connection/primitive scoping, lives in Policies.
Connection tools
Every active connection the session’s agent can act through contributes its catalog tools, registered as<connector>.<tool> (e.g. tracker.get_issue) — the same tools.configs key you write allow/ask/deny against.
MCP connector
Connect any MCP server as a toolset. Configuration is split so secrets never live on the agent definition. The agent declares servers by name and URL:tools wrapper, through configs entries keyed by the MCP tool name — the identical default_config + configs + permission pattern used for built-ins:
- Constraints: up to 20 servers per agent; every declared server must be referenced by at least one enabled tool, and every referenced tool must resolve to a declared server.
- Default permission is
askfor MCP tools, so a newly exposed server tool never auto-runs. To trust one, give it an explicit"permission": "allow"inconfigs— per tool, by name. There is no server-wide trust switch. - Auth is injected at session start, never on the agent: reference vault credentials (
static_bearerormcp_oauth) which are matched to servers by URL and injected server-side. The sandbox never receives the token. - A connection or auth failure doesn’t stop the session — it surfaces as a
session.errorevent naming the server, and the connection is retried on the next wake.
Vetta as its own MCP server
POST /v1/mcp publishes the Vetta API itself as an MCP tool catalog, so an agent can operate a
Vetta account the way it operates any other connected system. It is a JSON-RPC endpoint speaking the
three methods a catalog needs (initialize, tools/list, tools/call) and nothing else.
- The catalog is generated from the route table, one tool per served route, named
resource_verb—agents_create,sessions_list_events,computers_exec_command. Namespaced by the server, a tool isvetta.agents_createinconfigs, exactly liketracker.get_issueabove. - Argument schemas are the request schemas. Path parameters, declared query filters and body
fields arrive as one flat object, validated by the same zod schema
/v1/openapi.jsonpublishes. - Every tool carries
annotations.readOnlyHint, so a caller can allow reads andaskon writes without maintaining a list. - A tool call is an ordinary API call. It is dispatched back through the same middleware with the bearer that made it, so authentication, scope, the plan gate, rate limits and the strict request contract all apply once and identically. A tool can never reach further than its own key.
- Nine routes are withheld from the catalog and answer only over HTTP: this endpoint itself, the
SSE stream (
GET /v1/sessions/{id}/stream), the multipart upload (POST /v1/files), the four/v1/api_keysroutes (they mint and return live secrets),POST /v1/vaults/{id}/credentials(the one route that accepts a secret value) and the model proxy (a credentialed, billed model call is a loop, and its streaming half never returns). None of them is a thing a model should be handed.
Tools from your apps
Afullstack app that serves its own MCP endpoint declares the path once, on the app
("mcp": "/mcp" on create or patch; apps[].mcp in a naive.config), and nothing on any agent:
- Every agent that can access the app gets its tools. Access is the
appstool’sallowed_appsconfig — absent means every app in the organization, present means exactly the listed ids. On each turn the platform resolves the app’s current URL plus itsmcppath and registers the endpoint’s tools as<app-name>.<tool>(storefront.list_orders), so they are gated by the samedefault_config+configsallow/ask/deny as every other tool. No per-agentmcp_serversentry, and nothing to update when a custom domain moves the URL. - Auth is the platform’s own token. Setting
mcpmints an opaque bearer, pushes it into the app as the write-only secretVETTA_MCP_TOKEN— the app requires it on its endpoint — and injects it asAuthorization: Bearerserver-side on every call. It never reaches the sandbox, the transcript or any read route. Settingmcptonulldeletes the secret and forgets the token; re-sending the same path never rotates it. - An unreachable endpoint never fails a turn. An app that is not yet
active, or whose MCP endpoint is down or malformed, simply contributes no tools that turn; the platform logs one error line and the agent carries on with the rest of its toolset.
Coming soon
send_email/send_smsas agent tools — today email and SMS are sent via the API/CLI (POST .../send), not from inside a turn.- Custom tools — organization-defined, client-executed tools: you describe the arguments as a JSON Schema, the model emits a tool-use event, your application runs it and returns the result.
Context management
Keeping the context window small is the other half of cost control. Vetta manages it at three levels:Progressive disclosure
Skills load a small always-on index; full bodies are pulled only when a task needs them.
Automatic compaction
When the transcript crosses a token threshold, the earlier turns are summarized into a compaction block and the pre-summary history is dropped, preserving tool pairing.
Context editing
Stale tool results (and optionally older thinking) are cleared once they’re no longer needed, keeping the live window lean without losing the durable record.
seq cursor for at least 72 hours, so a consumer that falls behind can always resume without loss.
Next: files
Persist and retrieve artifacts across sessions.