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
Handoffs
Every agent carries ahandoffs 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_agentandlist_agentsare offered; any agent of the organization may be named, except itself."team"— both tools are offered, narrowed to the agents on this agent’smultiagentroster. Refused at save time on an agent with no roster.["scriptwriter", "producer"]— both tools are offered, narrowed to the names listed:send_to_agentoffers them as its choices andlist_agentsanswers with them.false(or an empty list) — neither tool is offered; the agent sends to nobody.
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, matchingweb_fetchandweb_searchbelow; 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 withproxy: true, so a proxied browser must name its domains.allow_writes—boolean,false. Off, the write guard refuses anact,clickortypethat looks like it submits or changes something.allow_extract—boolean,false. Off,extract,observe,linksandscreenshotare refused while a saved login is open (the extract guard).region— one ofus-west,us-east,eu-central,ap-southeast; defaultus-west. Where the browser runs.stealth—boolean,false. Anti-bot evasion.proxy—boolean,false. Residential proxy; needs a boundedallowed_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 tocontext_namewhen 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 underallow; see confirmation below.viewport,os,block_ads,solve_captchas— see the reference. Passed through to the browser as-is.
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, 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. 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 atGET /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
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. 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=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. 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_filefirst.
Looking at an image
Everything here that makes a picture files it and hands back an id: ascreenshot, 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.
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:
{ 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_imagedoes. An id your catalogue does not hold isnot_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.
diris relative and confined to the working directory, which is also the default; it is created if it does not exist. Adirthat 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. Thepathin 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.
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 permissionallow, 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 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.
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 samedefault_config + configs as every other tool:
email.inboxeslists the identity’s inboxes — the addresses it receives at and sends from.email.readreads 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.sendsends a plain-text email from one of them. It defaults toask, including when the agent’s default policy isallow, and is credit-checked like the send route.
<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: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.readOnlyHintandannotations.destructiveHint, so a caller can allow reads and benign writes andaskonly on the dangerous ones without maintaining a list.readOnlyHintfollows 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.destructiveHintistruefor 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_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_smsas 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.
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.