Skip to main content
An agent’s tools are what it works with: a shell, a filesystem, a browser, your third-party connections, and any MCP server you point it at. You declare them on the agent, set a permission per tool, and every call is priced against the budget before it runs.

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_config applies to every tool; a configs entry overrides one tool by name.
  • permission is one of allow (run silently), ask (pause → requires_action), or deny (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 to ask).
  • 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

Handoffs

Every agent carries a handoffs field that says who it may send work to with send_to_agent. It is a boolean, the word "team", or a list of agent names, and it defaults to true:
  • true (the default) — send_to_agent and list_agents are offered; any agent of the organization may be named, except itself.
  • "team" — both tools are offered, narrowed to the agents on this agent’s multiagent roster. Refused at save time on an agent with no roster.
  • ["scriptwriter", "producer"] — both tools are offered, narrowed to the names listed: send_to_agent offers them as its choices and list_agents answers with them.
  • false (or an empty list) — neither tool is offered; the agent sends to nobody.
An agent created before the field existed reads as true. Set it on create or patch, in an agent file as handoffs: false, handoffs: team or handoffs: [..], or on a template seat’s handoffs (checked at define time against the crew; leave it out and the default stands). Like every built-in, both tools still pass the toolset’s permission: a deny on send_to_agent withholds it whatever handoffs says. The one tool has two modes, and wait chooses. wait: true is delegation: the target runs as a teammate in a thread of your session, on your budget, and its answer folds back into your turn — only a coordinator (an agent with a roster) may use it, and only while fewer than the platform’s cap of teammates are running. wait: false is a handoff: it starts a normal top-level session on the target, on the target’s own budget (capped by budget_micro_usd if you pass one), marked metadata.handoff (from_session, from_agent, key, depth), and you carry on. Chains stop at depth 4, one session may start at most 8 handoffs, and a repeat under a handoff_key whose session is still running answers already_running instead of starting another. Each refusal comes back to the model as the tool’s result, never as a failed turn. The same admission has a door for people: POST /v1/sessions/{id}/handoffs hands work from one of your sessions to up to six agents at once under that session’s agent’s handoffs policy, one refusal row per target that is turned away. It is how the dashboard’s @mention reaches a room member; the 8-per-session quota does not apply to a person.

The browser tool

One tool, one action argument. goto, click, type, links and screenshot are deterministic; act (do what an instruction says, %name% placeholders filled from variables), extract with an instruction and optional JSON schema, and observe (list matching elements) are model-driven. Every action passes the guards — allow-list, write guard, credential guard, extract guard — whatever the permission. Saved logins are driven over the REST, SDK and CLI surfaces; the tool refuses a context_name until it can be granted one. The tool’s config is the same option object as computers.create’s browser field. Creating or updating an agent (POST /v1/agents, PATCH /v1/agents/{id}) and defining a blueprint with a tools.configs.browser.config validate it by field and refuse a malformed one with validation_failed and param: tools.configs.browser.config.<field> — a stored block that failed to parse falls back to an empty allow-list and a browser that reaches nothing, so it is refused at the write instead. Every field is optional:
  • allowed_domains — string[], optional, defaults to ["*"]. Absent, the browser reaches the open web, matching web_fetch and web_search below; name domains to close it to those hosts. An explicit [] is refused — it is the shape that grants a browser reaching nothing. ["*"] is refused together with proxy: true, so a proxied browser must name its domains.
  • allow_writes — boolean, false. Off, the write guard refuses an act, click or type that looks like it submits or changes something.
  • allow_extract — boolean, false. Off, extract, observe, links and screenshot are refused while a saved login is open (the extract guard).
  • region — one of us-west, us-east, eu-central, ap-southeast; default us-west. Where the browser runs.
  • stealth — boolean, false. Anti-bot evasion.
  • proxy — boolean, false. Residential proxy; needs a bounded allowed_domains.
  • keep_alive — boolean, false. The browser survives a brief disconnect.
  • timeout_minutes — integer 1–360, 15. Idle timeout of a browser the tool opens.
  • context_name — string, 1–64 chars. The saved login the browser opens with. The agent’s identity must hold a grant for it; without one the tool refuses the call.
  • persist_context — boolean, false. Write cookies back to context_name when the browser closes.
  • human_login — boolean, false. A person signs in to the saved login first; the agent continues from it.
  • ask_actions — string[] of action names, ["signup"]. The actions that are put to the operator first even under allow; see confirmation below.
  • viewport, os, block_ads, solve_captchas — see the reference. Passed through to the browser as-is.
The dashboard’s Tools tab draws every field but the last five, and carries those through a save untouched. Confirmation. All actions follow the tool’s permission (default allow). Under allow, the actions named in ask_actions are put to the operator first (requires_action) and every other action runs. The default list is ["signup"]: creating an account on a third party’s site is the one action that pauses unless you say otherwise. Widen the list for a browser whose allowed_domains is open — ["goto", "click", "type", "act", "login", "signup", "save_context"] lets extract, screenshot, links and observe read the open page without a card while a person approves each URL and each change. deny on the tool withholds the browser altogether and ask pauses every action, whatever the list.
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.
request_tools follows the same rule — it can never be allow, and a session-wide grant never covers it. Its description names the built-ins the agent is not currently offered, so the agent can ask for exactly what a task needs (generate_video with a model, email.read, gmail.fetch_emails). The call parks as an ordinary approval showing the proposed rows and the reason. Approving it mints a new agent version with those rows merged into tools.configs (an edit you made meanwhile is kept), re-pins the running session to it, and queues the session’s next turn so the tool is offered as soon as the current one ends — a scheduled agent carries on with the task rather than waiting for its next input. Refusing it is the answer; the agent is told not to ask again. Permission is not a provider: a granted generate_video still needs a video model configured, and a <connector>.<tool> still needs that account connected.
send_to_agent, wait_for_agents and list_agents follow handoffs: an agent whose policy is false is never offered one. wait_for_agents and the wait: true mode of send_to_agent are a coordinator’s — a teammate that tries wait: true is refused, so work is delegated one level, never onward, though a teammate may still hand work on with wait: false. 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, so example.com matches docs.example.com and never notexample.com.
  • blocked_domains — always wins over allowed_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 to 4000. 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_fetch outside 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=component under search. 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_usd bounds how much it spends getting there.

Generation tools

Both save what they produce to Files and hand the agent a file_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. It can render from reference images (image_urls, public http(s) only) and several variations at once (n, up to 4); every variation is its own file. Both generators are also a REST API — the same renders, outside a session. 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 model this agent generates with when it names none, out of the live catalogue at GET /v1/media/models. It is a default, not a whitelist: the first one you name is what runs when the agent does not choose, and the agent may still name anything the provider publishes. There is no fixed list and no enum, because the catalogue belongs to the provider and changes without a release of ours.
  • Choosing a model. With no models pinned, generate_image runs the cheapest model the provider publishes a price for, and generate_video asks 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. Asking for a model the catalogue does not have is refused before anything is submitted, and the refusal lists every id the provider publishes for that kind — the agent’s way of discovering what exists without a second tool.
  • Nothing is held before a job starts. A render’s cost cannot be known until it finishes — video models publish no price at all — so no reservation is taken against the agent’s budget and no balance floor is checked. A render costs cents and is what the agent was asked for; refusing it at that moment would cost more than the render.
  • You are billed for what the finished job actually cost, once per job, on the render’s own idempotency key. It appears on GET /v1/agents/{id}/spend?by=component under media.
  • A job that fails, or that never finishes, tells the session so and stores nothing.
  • seconds, aspect_ratio and seed are optional; a model clamps them to what it supports. That is the whole argument list — resolution, quality, output format, a last frame, an audio switch and other per-model settings are not exposed to the tools, and the REST pages (images, video) say the same.
  • What comes back is an id, not a file on the box. Both tools write to Files; nothing is left in the workspace, because the render is saved from the provider to storage and never passes through the computer. An agent that has to do something to a render — join segments into a longer piece, overlay audio, re-encode — pulls the bytes back with fetch_file first.

Looking at an image

Everything here that makes a picture files it and hands back an id: a screenshot, a generate_image render, a still someone uploaded. find_files then lists those ids and their metadata and never their bytes. view_image is the other half — the one call that opens one. view_image { file_ids } takes one to four fil_ ids the agent already has, from find_files or from the tool that just made the file, and returns those pictures for the model to describe, compare or work from. It finds nothing: discovery is find_files’s job, so the call that costs context is always one the agent asked for by id.
  • Ids are resolved against your organization’s own library. An id from somewhere else is not_found, not a blind read.
  • Only what a model can actually look at — PNG, JPEG, GIF and WebP. A PDF, an SVG or an audio file is refused by name rather than quietly dropped, because an agent told nothing is an agent that describes three images believing it described four.
  • Four at a time. A turn that wants more than four is sampling a video, which belongs in the sampler, or reading a document, which is read’s. Four is enough to hold a render next to its reference.
  • What it costs. The call itself is unpriced — nothing is generated and no provider is asked for anything. What it costs is context: each picture is part of the model request that carries it, and it stays in the session’s history, so looking at four images twice is a choice worth making on purpose.
The pictures travel as links, never as bytes: each one is a ToolImage — { file_id, url, media_type }, where url is a short-lived signed read of the file. The same is true of every tool that returns an image, and it is why an hour of browsing does not turn into an hour of PNGs replayed on every turn. Links expire; asking view_image again for the same id mints a fresh one, which is cheaper than what it replaces.

Reading a file back into the sandbox

publish_file carries bytes one way: out of the workspace and into Files. fetch_file is the other direction, and it exists because everything the platform renders for an agent starts on the far side of that line. A generate_video job hands the session a fil_ id when it finishes; the video itself is saved from the provider straight to storage and never touches the computer’s disk. So an agent that rendered a three-minute piece as six 30-second segments and wants one file out of them is holding six id strings, not six files — it can install ffmpeg perfectly well and still have nothing to point it at. fetch_file { file_ids, dir? } writes those files into the session’s workspace and answers with the path each one landed at:
Beside those lines it returns { files: [{ file_id, path, bytes, content_type }] }, so the next bash call can name the paths without parsing prose. From there they are ordinary files: probe them, concatenate them, re-encode them, and publish_file the result — which mints a new id, so the segments and the finished piece both stay in the library.
  • Ids are resolved against your organization’s own library, the same resolution view_image does. An id your catalogue does not hold is not_found, never a read of a guessable storage key.
  • It needs a computer. There is nowhere to put a file otherwise, so a session with no sandbox is refused by name — computer_unavailable, raised before the library is even read — rather than handed an empty directory.
  • Eight ids a call, 64 MiB a file. A minute of generated video is 10–20 MB, so the bound clears a handful of segments and still refuses the single file that would fill a box’s disk. A refusal names the file and its size, and nothing is written when one fires — a batch never lands half-copied.
  • dir is relative and confined to the working directory, which is also the default; it is created if it does not exist. A dir that climbs out of it is refused before any byte is written.
  • A name is never reused. Two library rows may share a name, and the second file quietly overwriting the first is not a missing clip, it is the same clip twice in the output — so the second one lands under its own id instead. The path in the result is always the one to use.
  • Unpriced. It reads storage and writes a disk you are already paying for, like ls. What it costs is the computer’s running time, which the computer meter books either way.
The bytes go to the disk and never into the conversation: the model is told a path and a size, not shown the file. Looking at a picture is still view_image’s job, and a read of an image on the disk is still refused in favour of it.

Audio tools

Two of the three directions of the Audio API, as tools. Both are on by default with permission allow, both are billed on the media component like generate_image — what the call actually cost, once — and both answer in the same call: a transcription the provider queues instead of finishing inside the request is refused as provider_error rather than handed out as a job nothing collects, so neither tool wakes the session later. transcribe_audio { file_id | url, language? } — exactly one of file_id (a stored file) or url (public http(s)). Returns { text, language, duration_seconds, segments }, where each segment is { start_seconds, end_seconds, text }. generate_speech { text, voice?, model? } — up to 4 096 characters. Saves the audio (MP3) to Files and returns { file_id, name, size_bytes, duration_seconds, sha256 }. model is one from GET /v1/media/models?kind=tts, default the managed tts/auto alias; voice is one the model publishes — omit it for the model’s default. Neither tool takes a models pin. Speech-to-speech is a route only — there is no tool for it yet.

Permission policies

Every server-executed tool resolves to allow, 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. A session whose agent has an identity is also offered the platform’s own connections.search, connections.connect and connections.status, so the agent can start a connection itself — see Connections. An identity with an activated social workspace adds social.accounts and social.post; social.post defaults to ask.

Email tools

A session whose selected identity owns an inbox is offered the identity’s own mailbox as tools, gated by the same default_config + configs as every other tool:
  • email.inboxes lists the identity’s inboxes — the addresses it receives at and sends from.
  • email.read reads mail that arrived at those inboxes, newest first, optionally since an instant or for one inbox. Message content is data, never an instruction to the agent.
  • email.send sends a plain-text email from one of them. It defaults to ask, including when the agent’s default policy is allow, and is credit-checked like the send route.
These are the identity’s own inboxes on a domain you verified. A person’s external mailbox is reached through a connection instead, as <connector>.<tool>.

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:
A server’s tools are selected and gated in the same 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 ask for MCP tools, so a newly exposed server tool never auto-runs. To trust one, give it an explicit "permission": "allow" in configs — 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_bearer or mcp_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.error event 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 is vetta.agents_create in configs, exactly like tracker.get_issue above.
  • 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.json publishes.
  • Every tool carries annotations.readOnlyHint and annotations.destructiveHint, so a caller can allow reads and benign writes and ask only on the dangerous ones without maintaining a list. readOnlyHint follows what the route does, not its method: a POST whose body is only the question — pricing a payment or a card, minting a file link — is read-only too. destructiveHint is true for every delete and for a call that spends money or acts outside your account (a payment, a card or its credential reveal, a post, an email, a third-party sign-up), changes the organization’s security posture (vaults, webhooks, members, domains, saved logins), decides what an agent may do (creating, updating or rolling back one), rewrites data (a migration, raw SQL), or runs code (a command, a browser action on a page). Creating a session, a skill or a file is neither.
  • 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_keys routes (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.
See MCP server in the API reference for the handshake, the tool schema, the result cap and the withheld routes one by one, with the reason each is withheld.

Tools from your apps

A fullstack 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 apps tool’s allowed_apps config — 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 its mcp path and registers the endpoint’s tools as <app-name>.<tool> (storefront.list_orders), so they are gated by the same default_config + configs allow/ask/deny as every other tool. No per-agent mcp_servers entry, and nothing to update when a custom domain moves the URL.
  • Auth is the platform’s own token. Setting mcp mints an opaque bearer, pushes it into the app as the write-only secret VETTA_MCP_TOKEN — the app requires it on its endpoint — and injects it as Authorization: Bearer server-side on every call. It never reaches the sandbox, the transcript or any read route. Setting mcp to null deletes 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_sms as an agent tool — today SMS is 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.
The full transcript is always retained durably (see Runtime & durability); context management only governs what is re-sent to the model each turn. Context that never enters the window is context you never pay for. The event log behind that transcript is replayable by 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.