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: deny-by-default (default_config.permission: "deny", then list the tools you want) and trust-by-default-except (allow all, set one tool to ask). default_config takes only permission; enabled: false is per tool, in configs.
  • A deny default does not deny the ask-by-default tools: an unlisted one still resolves to ask. Deny each by name.
  • 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, plus your company’s CEO. Refused at save time on an agent with no roster.
  • ["scriptwriter", "producer"] — both tools are offered, narrowed to the names listed plus the CEO: send_to_agent offers them as its choices and list_agents answers with them. An empty list reaches the CEO alone.
  • false — neither tool is offered; the agent sends to nobody, not even the CEO.
Every agent can always hand work to the company’s CEO, whether or not its list or roster names it. false is the only way to opt out. 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.

The mobile tool

One tool, one action argument, and the same actions as POST /v1/mobile_devices/{id}/act: look with screenshot or element_tree, then tap (x/y in screenshot pixels, or a selector by text, resource_id or content_desc), type, press_key, scroll (direction, optional amount) or open_url. end hands the phone back. The tool is built on the pi harness only, and open_url is not bound by the browser’s allowed_domains.
  • One phone per session. The first call opens it. Calls made in parallel share it. It stays open across replies and approvals, so the next message can carry on where the app is. It ends when the agent calls end, when the session ends or stops on its budget or an error, or when its idle timer fires. A later call then opens a new one.
  • The model sees the screen. A screenshot is saved as a file and shown to the model as a link to it, the way the browser’s screenshots are; a model without vision works from element_tree.
  • You can watch it. While the agent uses it, the phone appears in the chat’s Computer panel, below the agent’s computer, with its screen and Live view — and on the dashboard’s Mobile page (Computers → Mobile).
  • Or start it yourself. With no phone yet, the Computer panel offers Create phone. The phone is made with the agent’s config and filed under the chat (session_id on POST /v1/mobile_devices); the agent’s next call uses it instead of opening a second one.
  • Money. A phone bills $0.039 per minute on the mobile line, booked once when it ends against the organization’s balance. It is not held against the session’s or the agent’s budget cap; its timers bound it.
  • Permission. ask by default: the first call waits for a person. Approve it for the session and the rest of the chat runs without a card. Set allow to skip the card, or deny to withhold the tool.
config is optional and validated by field on every agent write:
  • os_version — Android only, 13, 14 or 15, default 15.
  • model — phone, the default and today the only value.
  • jurisdiction — us, eu or as, default us.
  • idle_timeout_minutes — 1–180, default 10. Also bounds what a long wait between messages can cost.
  • max_duration_minutes — 1–60, default 60. An agent’s phone is capped at an hour: the minutes settle against the session when it ends.
  • apps — up to 10 package names from the platform’s curated app catalogue, installed when the phone opens.
ask_operator has no allow (nor do request_tools, propose_plan and apps.request_access). 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, and mobile is offered only on a deployment that can provision phones. 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 up to three reference images (image_urls, each a stored file’s fil_ id — say one an earlier render returned — or a public http(s) URL) and several variations at once (n, up to 4); every variation is its own file. On the platform’s own image models, when some of them fail the ones that rendered are still saved and billed; elsewhere a model renders the set or fails it whole. 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 platform’s default image model — the first the catalogue lists that takes what the call carries (a reference image, a character) — and, if that model is unavailable, the next one; generate_video runs the first of the platform’s video defaults the catalogue publishes — minimax/hailuo-3, bytedance/seedance-2.5, then alibaba/wan-3.0/text-to-video — and for an edit, only one the catalogue says takes a video; otherwise the agent is asked to name one. A model the agent names, and a pin, always win, and never fall back to another. 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 render starts. 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, and refusing it at that moment would cost more than the render. Training a character is the exception — it is held first.
  • You are billed for what the finished job cost, once per job, on the render’s own idempotency key — a replayed call renders and bills once. A video on a model priced per generated second costs its length at the resolution it renders at (720p on the platform’s per-second models); one whose model reports a cost costs that. 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. The platform’s own models clamp them to what they support — an aspect ratio or a length a model does not take becomes the nearest one it does; another model may refuse a value it does not take instead. That is the whole shared 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. Both tools and both routes also take character (and generate_image style and character_strength), below.
  • 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.

Editing a clip: edit_video (Mira)

edit_video is Mira. It edits a 3–15 s clip from a prompt and up to 4 references (a fil_ id or a public URL each): a new place, an outfit or object, a person swapped for one in a reference photo. Motion, camera and the filmed look are kept. Optional mode. One call makes one edit. Like generate_video it runs in the background; the result lands in Files.
  • It comes with generate_video. An agent offered generate_video is offered edit_video on the same terms (allow, ask or deny). Name edit_video in tools.configs to set it on its own. It has no row of its own in the dashboard.
  • A deployment that does not run the edit lane offers it to nobody.
  • It has a set price: $0.03 per second of the source clip, rounded up, for an edit with no photos; $1.48 for an edit with photos or a person swap, at any length (pricing). Failed, refused and timed-out edits cost nothing.

Characters

A one-shot generate_image of “a woman with freckles” is a different woman every time, so a channel of shorts has no recurring host. A character fixes that: a reusable, consistent on-screen persona trained once from a handful of reference stills and then pinned into every later render, so the same face, hair and build carry across a whole channel — the AI-influencer primitive. It is the media sibling of an identity: an identity is who an agent is being to the world (an inbox, a number); a character is who it renders as on screen. create_character { name, description?, image_urls, persona?, style_pack?, consent } starts the training and does not block — a character trains for minutes, far longer than a turn. It returns a chr_ handle in status: "training" and the agent carries on.
  • name — a short name, unique among the organization’s characters, how the agent refers to it later (“Ava”); up to 100 characters. Surfaced to the model.
  • description — optional; who the character is (looks, age, vibe, wardrobe), so renders stay in character.
  • persona — optional; up to five lines (niche, bio, voice, wardrobe, hooks) an agent reads when it writes as the character.
  • style_pack — optional; up to 12 look names the character is usually drawn in; none gives Phone snap, Realistic and Everyday.
  • image_urls — one to sixteen reference photos of the same person, each a stored file’s fil_ id or a public http(s) URL; more, varied angles train a stronger likeness. A private host, a data: or file: URL, and a fil_ that is not one of the organization’s images are refused. The first two are checked for a recognizable public figure before anything is held; if they show one, the call is refused with the reason.
  • consent — required, { attested: true }: the person in the photos is the operator, or has agreed to an AI likeness, and is not a public figure. The agent sets it only once a person has confirmed it; the character keeps it as metadata.consent with the session, the time and whether a person allowed the call on its card.
A training is paid, so create_character always asks — the person approving sees whose face is being trained, and the card says that their yes confirms the person in the photos agreed. No tool policy can set it to allow (an allow reads as ask; deny still removes it), the tool refuses a call no person approved, and a session grant does not wave later trainings through. It is priced per training on the media component: a hold is taken against the session’s budget before anything is bought (a budget with no room refuses it, and so does a per-task ceiling below the price), and the training is billed once the trainer accepts the photos — a training that later ends failed is refunded. A name a live character already holds is refused before the trainer is paid, and a replay of the same call answers the character it already made, untrained and unbilled. get_character { character } reads its status by chr_ id or name. Poll it until the status is ready — the only renderable state — or failed (train a new one with clearer references). list_characters lists every character the organization has, so a host trained in one chat is found in the next, then the prebuilt ones every organization can render as with no training (they cannot be deleted); delete_character { character } retires one and frees its name. All three are free. Then render:
  • generate_image takes an optional character argument — the chr_ id or name of a ready character. The same face then carries across every image. With image_urls too, each one a still of that character, the image is an edit of that still that keeps its room, props and outfit in a new pose or framing — how a later shot of the same scene starts. Add style to draw it in a look, and character_strength (0.5–1) only when a face drifts in a close-up.
  • generate_video takes character too, with image_urls set to one still rendered as that character, which becomes the first frame: the face on screen is the still’s, so the still is required, and only a model that animates a still is used. There is no video model that pins a character directly.
A character is refused before anything is rendered when it is still training or its training failed (validation_failed), and both tools and the character argument are refused with feature_not_configured on a deployment whose media backend trains no characters — a character is never silently dropped, which would render the wrong face. The same lifecycle is a REST API — POST /v1/characters, and character on POST /v1/media/images — and vetta character in the terminal; a character made on any of them renders on all of them.

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, 5 GiB a file — the same ceiling as publish_file, so anything an agent published it can fetch back. Bytes cross a slice at a time and are never held whole on the way, so size costs transfer time only. 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. generate_sound_effect { prompt, duration_seconds?, loop? } — a prompt up to 500 characters describing the effect, an optional duration_seconds (0.5–30) and an optional loop. Saves the audio (MP3) to Files and returns { file_id, name, size_bytes, duration_seconds, sha256 }. Omit duration_seconds to let the length follow the prompt. Billed on the media component like generate_speech. generate_music { prompt, length_seconds?, instrumental? } — a prompt up to 2 000 characters describing the song, an optional length_seconds (3–300) and an optional instrumental. Saves the audio (MP3) to Files and returns { file_id, name, size_bytes, duration_seconds, sha256 }. Omit length_seconds for a 30-second song. Billed on the media component like generate_speech. None of these tools take 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. The tools that publish, spend, file or change the company’s settings ask by default: social.post, email.send, legal.verify, legal.resend_link, legal.form, legal.submit, wallet.pay, wallet.transfer, card.issue, card.credentials, card.cancel, company.set_timezone, company.set_logo, company.set_brand, apps.request_access, mobile, data_run and create_character. create_character, apps.request_access, company.set_logo and company.set_brand never run without a card, whatever the policy. For the rest the order is: a per-tool configs entry wins; otherwise the tool is ask unless default_config.permission is deny, in which case it is deny. To offer one to such an agent, give it "permission": "ask" in configs.

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, social.post, social.status (one published post’s status and per-network analytics, by the id social.post returned) and social.post_metrics (how the organization’s recent posts are doing, each metric against the identity’s usual); 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>.

Company settings tools

Every session is offered three tools that change the calling session’s own organization, never another. All three default to ask, including when the agent’s default policy is allow, and none is ever granted for the rest of a chat.
  • company.set_timezone takes { timezone }, an IANA name, and sets the company’s timezone.
  • company.set_logo takes { svg } and sets the company’s logo, shown in the studio’s top-left corner and in its apps. It always asks, whatever the policy, and only a signed-in owner or admin can approve it. The agent writes the SVG by hand: one <svg viewBox="0 0 64 64"> of flat shapes. The platform checks it with the same rules as PATCH /v1/organizations/{id}; a refusal comes back as the reason, and nothing is written.
  • company.set_brand takes { colour, harmony?, typeface, tagline? } and sets the company’s brand kit: its main colour as #rrggbb, a palette harmony from 0 to 3, a typeface (grotesk, serif, rounded or mono) and a line of up to 80 characters. The platform builds the palette from the colour. Like the logo, it always asks, and only a signed-in owner or admin can approve it.

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:
  • The url must be a public http or https address. A private, loopback or link-local IP, localhost, or a .local or .internal name is refused with 400 validation_failed, param: "mcp_servers.0.url".
  • Constraints: up to 20 servers per agent; server URLs must use HTTPS; 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. Secret-bearing inline headers such as Authorization, cookies and API keys are rejected; the sandbox and model never receive the token. A credential bound to an agent (agent_id) reaches only that agent.
  • Catalog input is bounded: one server contributes at most 100 tools; names, descriptions, schemas and response bodies are capped before they enter model context. Configured credentials are redacted from catalog metadata, tool output and setup failures.
  • The server name vetta and this platform’s own /v1/mcp are reserved: declaring either needs the admin scope, and is refused with 403 forbidden otherwise.
  • 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.

Naive Managed Agents as its own MCP server

POST /v1/mcp publishes the Naive Managed Agents API itself as an MCP tool catalog, so an agent can operate a Managed Agents 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. Managed Agents 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.