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: deny-by-default (
default_config.permission: "deny", then list the tools you want) and trust-by-default-except (allow all, set one tool toask).default_configtakes onlypermission;enabled: falseis per tool, inconfigs. - 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 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, 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_agentoffers them as its choices andlist_agentsanswers with them. An empty list reaches the CEO alone.false— neither tool is offered; the agent sends to nobody, not even the CEO.
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, 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.
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
screenshotis 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 fromelement_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
configand filed under the chat (session_idonPOST /v1/mobile_devices); the agent’s next call uses it instead of opening a second one. - Money. A phone bills
$0.039per minute on themobileline, 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.
askby default: the first call waits for a person. Approve it for the session and the rest of the chat runs without a card. Setallowto skip the card, ordenyto withhold the tool.
config is optional and validated by field on every agent write:
os_version— Android only,13,14or15, default15.model—phone, the default and today the only value.jurisdiction—us,euoras, defaultus.idle_timeout_minutes— 1–180, default10. Also bounds what a long wait between messages can cost.max_duration_minutes— 1–60, default60. 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, 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 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 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 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_videoruns the first of the platform’s video defaults the catalogue publishes —minimax/hailuo-3,bytedance/seedance-2.5, thenalibaba/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=componentundermedia. - A job that fails, or that never finishes, tells the session so and stores nothing.
seconds,aspect_ratioandseedare 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 takecharacter(andgenerate_imagestyleandcharacter_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_filefirst.
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 offeredgenerate_videois offerededit_videoon the same terms (allow,askordeny). Nameedit_videointools.configsto 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-shotgenerate_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’sfil_id or a publichttp(s)URL; more, varied angles train a stronger likeness. A private host, adata:orfile:URL, and afil_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 asmetadata.consentwith the session, the time and whether a person allowed the call on its card.
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_imagetakes an optionalcharacterargument — thechr_id or name of areadycharacter. The same face then carries across every image. Withimage_urlstoo, 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. Addstyleto draw it in a look, andcharacter_strength(0.5–1) only when a face drifts in a close-up.generate_videotakescharactertoo, withimage_urlsset 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.
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: 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, 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. 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.
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 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.
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 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>.
Company settings tools
Every session is offered three tools that change the calling session’s own organization, never another. All three default toask, including when the agent’s default policy is allow, and none is ever granted for the rest of a chat.
company.set_timezonetakes{ timezone }, an IANA name, and sets the company’s timezone.company.set_logotakes{ 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 asPATCH /v1/organizations/{id}; a refusal comes back as the reason, and nothing is written.company.set_brandtakes{ 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,roundedormono) 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:tools wrapper, through configs entries keyed by the MCP tool name — the identical default_config + configs + permission pattern used for built-ins:
- The
urlmust be a publichttporhttpsaddress. A private, loopback or link-local IP,localhost, or a.localor.internalname is refused with400 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
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. Secret-bearing inline headers such asAuthorization, 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
vettaand this platform’s own/v1/mcpare reserved: declaring either needs theadminscope, and is refused with403 forbiddenotherwise. - 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.
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 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. 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.
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.