Skip to main content
Glama

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
MANDALA_TAGSNoComma-separated tool tags to keep; see Tool filters in the README.
MANDALA_API_KEYNoAPI key from Settings, API keys. Optional after mandala login, which saves one.
MANDALA_PROFILENoSaved profile to use; --profile overrides it.
MANDALA_BASE_URLNoAPI base URL. With a saved profile, must match its stored base.https://app.mandala.computer/api/v1
MANDALA_MODEL_KEYNoAn Anthropic or OpenAI key. Enables run_agent on that key; without it, run_agent runs on the account's AI credits when they can pay.
MANDALA_READ_ONLYNo1 or true keeps only read-only tools.
MANDALA_COMPUTER_IDNoBind a computer at startup, so use_computer is not needed.
MANDALA_NO_LIFECYCLENo1 or true withholds every tool that creates or destroys a computer or snapshot.

Instructions

Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.

This server publishes no instructions, or was last inspected before Glama recorded them.

Capabilities

Features and capabilities supported by this server

Protocol revision2025-11-25

CapabilityDetails
tools
{
  "listChanged": true
}
logging
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
whoamiA

Who the API key this server runs with belongs to: the person (id, email, name), the account it acts on (id, name, plan, status: active or suspended), the role it acts with now (owner, member or viewer), the workspace it is confined to (null for the whole account), and the key itself (id, name, prefix, and manage_keys: whether it may manage API keys). For a key confined to a workspace the email, both names and the plan are null: the platform withholds them from a key that may be in the hands of an end customer. Needs no permission and any role; a suspended account can call it. No arguments.

list_api_keysA

The API keys of the person this server's key belongs to, on the account it acts on, newest first: id, name, prefix (display only), scope, when last used, manage_keys, and minted_by_key_id: the key that minted this one (null for a key minted from the dashboard); revoking a key does not revoke the keys it minted. Never a raw key. Needs this server's key to be allowed to manage keys, which only a person can turn on in the dashboard (Settings → Credentials → API keys → the key's menu → Allow managing keys); without it the answer is a 403 that says so — relay it rather than retrying. A key confined to a workspace sees only keys confined to that workspace. Minting and revoking keys are deliberately not tools here, so a raw key never lands in this transcript and a model never revokes one on its own reading of this list: a person does both in the dashboard or with the mandala CLI (mandala api-keys create / revoke).

list_workspacesA

The account's workspaces, oldest first: id, name and created_at. A workspace partitions the account's computers: a key confined to one reaches that workspace's computers only, and a computer's workspace_id names the one it is in. A key confined to a workspace lists only that one. create_workspace, rename_workspace and delete_workspace change them. No arguments.

get_workspaceA

One workspace: id, name and created_at. An id this key cannot see is not found, the same as one that does not exist — another account's, any workspace but its own for a key confined to one, and a deleted workspace a computer's workspace_id can still name.

list_workspace_membersA

The people who reach a workspace, oldest member first: user_id, email, name (null when unset), role, accepted_at, suspended (a sign-in suspension: they cannot sign in anywhere) and seat_suspended (true while they wait for a seat on this account and cannot act on it; it returns to false when their seat is restored). Either flag, or both, can be true. Workspaces do not divide people — everybody on the account reaches every workspace at their account role — so this is the account's accepted members; invitations not yet accepted are left out. A key confined to a workspace is refused with 403, because the list is the whole account's: relay that rather than retrying, and use an account-wide key.

create_workspaceA

Make a workspace on this account: a partition of its computers, which API keys can be confined to. Answers its id, name and created_at. A name the account already uses, a blank or over-long one, or an account already holding 500 workspaces is refused with 400. Needs an owner's key that is not confined to a workspace: a member, a viewer, or any key confined to a workspace is refused with 403 — relay that rather than retrying. NOT safe to repeat blind: if the answer is lost, list_workspaces before creating again, since it may exist.

rename_workspaceA

Rename a workspace. Its id does not change, so the API keys confined to it and the computers in it are untouched. The name follows create_workspace's rules; a name the account already uses is refused with 400, and an id this key cannot see is not found. Needs an owner's key that is not confined to a workspace: a member, a viewer, or any key confined to a workspace is refused with 403 — relay that rather than retrying.

delete_workspaceA

Permanently delete a workspace. It REVOKES every API key confined to it, whoever holds it, in the same step: anything using one of those keys — a person's CI, another agent — is refused from its next request, and this cannot be undone. The answer says how many were revoked. Webhook subscriptions confined to the workspace (those created with a key scoped to it) are deleted with it in the same step, with every delivery record they hold, pending ones included, as delete_webhook does; the answer does not count them, and account-wide subscriptions are not affected. Only an empty workspace can be deleted: one that still holds computers (counting one an unfinished workspace change is moving into it) or active secrets is refused with 409, saying how many, and nothing is deleted or revoked. To empty it, move its computers to another workspace with move_computer_to_workspace (or in the dashboard) or delete them, and delete its secrets with delete_secret, giving their workspace_id and current revision_id; then send this again. Say what is in it before moving or deleting anything to empty it. A 503 means the platform could not confirm it is empty: nothing was deleted, and it can be sent again shortly. Snapshots taken in it remain, and an account-wide key can still clone them. Needs confirm: true. Needs an owner's key that is not confined to a workspace: a member, a viewer, or any key confined to a workspace is refused with 403 — relay that rather than retrying.

get_accountA

Read account-wide plan ceilings, per-computer maxima, current consumption and advisory remaining quota. Viewer or stronger; workspace-scoped keys receive aggregates without resource identities. No arguments, selected computer or model key. Incomplete computer/snapshot groups have null consumption and remaining values, meaning unknown. Snapshot headroom covers indexed stored bytes, excludes in-flight capture reservations and cannot promise that a capture will fit. This observation reserves nothing; later mutations still enforce quota.

list_templatesA

The base images a computer can be created from — name, OS, the default CPU and RAM each one implies, and its minimum disk (a smaller disk_gb is raised to it and charged at it). For the templates the platform publishes, name is what a create takes as template; for one you published yourself, pass its ref (namespace/name@version) — a short name is not resolved to your own templates. A short name that is none of the published templates falls back to base, except while this list is INCOMPLETE (a host did not answer): then the create is refused with a retryable 503 that changed nothing, so send it again shortly with the same idempotency_key, or pass a ref to avoid the guess.

list_sizesA

The named sizes a computer can be launched at — each a template plus a CPU/RAM/disk shape. These are the shapes the platform keeps pre-booted, so create_computer with a size is typically answered in about a second where a custom shape boots cold. allowed says whether this account’s plan admits a row; when false, cheapest_plan names the plan that would.

list_computersA

Every computer on this account that exists or may exist. Desktop credentials are deliberately not included — use get_desktop_url for those. Read state before acting on a row: it is the platform's record of whether the machine exists, which is a different question from status, what its host says the guest is doing. A row reading deleting is on its way out and is not one to bind, start or wait for, and one reading unreachable is a row served from the record because the host did not answer — the computer is most likely fine, but nothing only its host knows is on it, status included. The two terminal states are not here at all: deleted and lost come back only when asked for with state.

get_computerB

Everything the platform knows about one computer: status, size and its recorded screen resolution. Click coordinates are the capture size a screenshot returns, which can differ from the record's resolution.

use_computerA

Bind a computer to this session, so every later call can leave computer_id out. Answers with its status and screen resolution.

start_computerB

Boot a computer, or resume a suspended one — a resume restores the saved session, same processes and windows, in about a second. A resumed guest's clock is set to the current time within a few seconds, best effort through its guest agent (one whose agent does not answer keeps the old time until NTP corrects it), and the desktop panel's clock can show the old time for up to a minute after that.

suspend_computerA

Write the guest's RAM to disk and give the host its memory back. A pause, not a stop: start_computer resumes the same session. A computer that holds secrets suspends only on a host that seals saved sessions (its memory is encrypted on the way to disk); elsewhere, and while its secrets are still being delivered, it is refused with 409. After a change to its secret bindings that dropped a secret or moved one to another revision while it ran, it cannot be suspended until it restarts, since its memory may still hold the old value.

restart_computerA

Reset the computer, like the reset button — not a fresh boot, so a change that needs a new machine shape (a resize) needs a stop and a start instead. A suspended computer is refused with 409: start it to resume, or stop it to discard the session. A stopped computer with no secrets bound is started instead; one with secrets bound is refused with 409 while stopped (start it, which delivers them) or while its secrets are being delivered, and otherwise gets its secrets again as it comes back up, a few seconds after it reads running: wait_for_computer with until="guest" waits for them where the platform reports that redelivery, and where it does not, a command run in those seconds can see them unset. A restart issues new desktop credentials and closes open desktop connections, so a get_desktop_url link from before it stops working.

stop_computerA

Shut a computer down: the guest is asked, and given up to 20 seconds to do it; a guest still up after that is forced off by the platform, so an ordinary stop always ends stopped and can take up to about a minute to answer. On a suspended computer this discards the saved session, and the next start is a fresh boot. The disk is kept. force skips the asking and the 20-second wait and pulls the power at once.

update_computerA

Change a computer's name, its size, its idle window, its browser proxy, or its egress proxy. The platform refuses these in combination on purpose — a resize needs the computer stopped and the others do not, so one request cannot honour both without applying half of it. A SUSPENDED computer counts as stopped for a resize, and its saved desktop cannot survive one: the vCPU count and the memory size are part of the saved state, so it is discarded and the next start is a cold boot. Resume it and finish what is open before resizing, or say so before you do it.

move_computer_to_workspaceA

Change which workspace a computer belongs to, within the same account. This changes who can access it. Requires an account-wide member or owner credential; workspace-scoped credentials are refused, even for a no-op. Supply computer_id explicitly and workspace_id as a wsp- ID, or null to make it unassigned. Neither the selected computer nor the default workspace is used. The computer must be stopped, with no saved session, snapshots or snapshot history copies, secret bindings, or proxy credentials. The platform checks these conditions; a fleet that cannot prove safety can refuse. Do not stop the computer, discard its session, delete snapshots, or remove bindings automatically to overcome a refusal. This changes workspace membership, not hardware or size. Once moved, keys confined to the old workspace lose it, its events and webhooks follow it, and its desktop and share credentials are replaced, so a get_desktop_url link from before stops working. If the outcome cannot be confirmed, read get_computer before deciding whether to retry; repeating the same call reconciles a change whose outcome was uncertain.

move_computerA

Grow a computer past what its current host can run, by moving it to another host in the same region first. Only call this after update_computer has refused a resize and said a move is possible: it is the second half of that refusal and nothing else. THIS MOVES THE MACHINE TO DIFFERENT HARDWARE and copies its disk to get there — say so before you call it. The computer must be STOPPED (suspended is not stopped here: a saved desktop only loads on the host that wrote it, so resume and stop it, or discard the session). One move runs per account at a time. Everything is decided again when this runs, so it can still refuse. Waits for the outcome and reports it, reporting progress while it waits so a client that sends a progressToken and sets resetTimeoutOnProgress can hold the request open; list_moves reads the outcome if the wait runs out, and is the answer for a client that cannot.

list_movesA

Every move on this account that this key can see — a key confined to a workspace sees only the moves of computers in that workspace: the ones running and the ones that finished in the last day. Read this after move_computer if the wait ran out, and read it when a move is refused because another computer on the account is already being moved — only one runs at a time, and this says which one and how far along. live is the flag to poll on.

get_usageA

What this account has spent: running hours weighted by cores and memory, the storage it holds, and the per-computer breakdown behind the totals — the same figures the billing page shows. Read it before and after a batch of computers to know what one cost, and read it when asked how much anything has cost. Defaults to the current billing period, which is the window an invoice covers. READ THE FIRST LINE OF THE ANSWER: a hypervisor that could not be reached makes every total too LOW rather than absent, and that line is the only thing that says so.

wait_for_computerA

Poll until the computer is running, or until the software inside it answers. Use "guest" before exec, files or windows, and before expecting a screenshot to show a desktop rather than a boot screen; on Linux "guest" also waits until the desktop session is logged in, so exec with desktop: true is not refused as having no desktop session; on a computer with secrets bound, "guest" also waits until they have reached the desktop, so a command run next sees them, on one whose browser proxy is being set or removed it waits until its browsers have the change, and on one whose egress proxy is waiting for its credentials it waits until they arrive, since every connection the computer opens meanwhile is closed. Reports progress while it waits, so a client that sends a progressToken and sets resetTimeoutOnProgress can hold the request open; a client that cannot should lower timeout_s and call again rather than watch its own default timeout cancel the wait.

get_desktop_urlA

A URL that opens this computer's live desktop in a browser. Watch-only by default. These are credentials in a link: anyone holding one has that desktop until the computer restarts or is moved to another workspace, either of which issues new desktop credentials.

create_computerA

Build a new cloud desktop and select it for this session. Creating and running a computer costs money on this account. Continue an image preparation refusal only as its result instructs, retaining all original create arguments, including template, and adding the returned preparation token as an argument. This token is not a create idempotency key. Stop after success; never automatically replay after a lost or ambiguous response without the idempotency_key that response names — with it, the same call answers the first one's result instead of building a second computer.

clone_computerA

Copy a computer's current disk to a new computer — the fork half of snapshot-and-fork. The source is untouched and must be stopped or suspended: a running source is refused with 409. The answer arrives as soon as the copy exists, reading status "building" while its disk is copied — wait for that to finish before starting it. The copy keeps the source's size and resolution (its disk carries a desktop laid out at that size), lands stopped, and has no secrets bound.

delete_computerA

Destroy a computer and its disk. Irreversible. Its snapshots are kept by default and become orphans, which can still be cloned but not restored. To destroy those too, read snapshot_holdings first and pass its fingerprint as expect.

get_operationA

One lifecycle operation: what an accepted create, clone, start, stop, suspend, restart, snapshot restore, resize, move or delete started, and where it got to. A call made with an idempotency_key is recorded pending before it is carried out, so it can be found even when its answer was lost; one the platform never heard end is failed with error.code lost. state is pending or running while live, and succeeded or failed once final; a failed one carries error.code (start_failed, build_failed, computer_gone, move_failed, resize_not_applied, lost, and more may be added) and a sentence. Most are already succeeded when the call that started them answered; a clone is running until its disk is copied, and a move until it lands. succeeded means the platform finished its step, not that the desktop has booted: wait_for_computer is still the wait for a desktop that answers. An id this key cannot see — another account's, or for a key confined to a workspace, one about a computer outside it — or one that has expired, is not found.

list_operationsA

The lifecycle operations started on this account, from the API or the dashboard, newest first, a page at a time: kind, computer_id, state, error and timestamps. A key confined to a workspace sees only the operations of computers in that workspace. computer_id keeps one computer's (for a clone, the new computer's) — omitted, it is every one this key can see, not the selected computer's. Pass next_cursor back as cursor for the next page; it is null on the last. Calls made from the dashboard are listed too, except a move.

wait_for_operationA

Poll a lifecycle operation until it is final. Answers once it has succeeded; a failed one is an error carrying error.code and the platform's sentence (resize_not_applied is a move that landed at the old size: the computer has moved, and update_computer finishes the resize). succeeded means the platform finished its step, not that the desktop has booted: wait_for_computer is still the wait for a desktop that answers. Most operations are already final when their call answers, so this is for a clone (running until its disk is copied) or a move. Reports progress while it waits, so a client that sends a progressToken and sets resetTimeoutOnProgress can hold the request open; get_operation reads it if the wait runs out.

screenshotA

A picture of the screen, returned as an image with its capture name and original size when available. Fresh by default; give capture to crop the earlier frame you measured. Full-size capture pixels are the coordinates click, drag and scroll take; cropped or scaled pictures include a mapping. A saved frame of a suspended computer (served with fresh: false) is labelled as one: a downscaled picture of the screen as it was, not in click coordinates.

clickB

Click the mouse. Left button once by default. Take a screenshot afterwards to see what happened — the desktop does not report back on its own.

type_textA

Type a string into whatever has keyboard focus: literal text, 1 to 400 characters. For Enter, Tab or a shortcut, use press_key. Plain ASCII is typed as US-layout key events, about 12 ms a character, so 400 characters take about five seconds. Text containing any other character — accents, CJK, emoji — is typed WHOLE and in order by one guest helper: its ASCII as the same key presses and the rest by GTK Unicode composition. That works in Chromium (GTK3) and Xfce Terminal on a Linux X11 desktop; Firefox, GTK4 apps, native Wayland apps and Windows are unsupported for non-ASCII text, and such text is checked and refused before any key is pressed. Tab and newline are sent as keys and CRLF as one Return; a bare CR and other control characters are refused. The answer says which way it was typed. It confirms the keys were sent, not that the application accepted them: check with a screenshot before relying on it. For longer text, or non-ASCII text in an application this cannot type into, paste_text puts it on the clipboard and pastes it in one step.

paste_textA

Put text on the desktop clipboard and press the paste shortcut, as one step: the fast way to insert up to 8192 bytes of UTF-8, including accents, CJK and emoji, into whatever has focus. Linux desktops only; Windows and a guest without working clipboard support are refused. The paste REPLACES the clipboard and leaves the text there — after pasting a secret, clear it with write_clipboard and text "". A success means the clipboard write and the shortcut were delivered, not that the application inserted the text (a paste-disabled field ignores it): check with a screenshot. An interrupted call may already have pasted, so look before sending it again, and do not fall back to type_text without looking.

press_keyB

Press named keys — "Return", "Tab", "Escape", "Page_Down", or a chord like ["ctrl","c"]. X keysym names, so "Return" rather than "Enter".

scrollC

Turn the wheel. With no coordinate it scrolls whatever is under the pointer; with one it scrolls what is at that point.

dragA

Press, move and release as one gesture — for selecting text, moving a file, or dragging a slider. Not the same as two clicks. modifiers holds keys down for the whole gesture, e.g. ["shift"] to extend a selection.

move_mouseA

Move the pointer without clicking — for hovering over a menu or a tooltip.

mouse_buttonB

The two halves of a click, for gestures drag cannot express — a lasso across several stops, or a press held while the keyboard is used.

cursor_positionA

Where the pointer was last put. known is false on a computer nothing has moved it on yet — the guest cannot be asked, so there is no coordinate to report and a confident 0,0 would be a wrong answer dressed as a right one. This reads a coordinate but reaches the computer to do it: a suspended one is resumed to answer, and that resume is charged. read_clipboard, by contrast, refuses a suspended computer rather than starting it.

waitA

Pause before looking again — for a page that is still painting or an application that is still starting. Capped at 30 seconds, because a wait here is a held HTTP request and a longer one would not return, it would fail.

execA

Run a shell command inside the computer. Runs as root with no display by default — anything that opens a window needs desktop: true, anything slower than a few seconds needs background: true, and anything that needs an environment variable takes env rather than an assignment written into the command. Against the hosted platform, waiting here is capped at about two minutes by a proxy in front of it, not by timeout_s. One computer runs at most sixteen background commands at once: the seventeenth is refused rather than queued, and exec_kill on a pid you already hold is what makes room. What the command printed comes back as stdout and stderr when it is text, and as stdout_b64 and stderr_b64 — base64 of the exact bytes — when it is not, so a command that emits binary is never quietly turned into replacement characters.

exec_pollA

What a backgrounded command has printed since the last time you asked, and whether it has finished. The output is a cursor, not a buffer: each poll gives you only the new bytes, so two readers on one pid split the output between them rather than each seeing all of it. Finishing is also what releases a computer's background slot, so this is how you tell which of your handles still hold one — the poll itself frees nothing. Output comes back as stdout and stderr when it is text and as stdout_b64 and stderr_b64 when it is not; a chunk cut through the middle of a character keeps both halves, with the text stopping at the last whole one and a note naming the bytes the next poll begins with.

exec_killA

Kill a backgrounded command and everything it started. Answers with its final state, including whatever it printed that you had not read. Also how you make room when a computer is already running its maximum of sixteen background commands: the slot comes back with the command.

open_urlA

Put a web page on the screen in the guest's browser: the first of firefox-esr, firefox or chromium the image has (the Debian desktops open firefox-esr, Omarchy opens chromium). An image with none of them is an error, not a silent success. The command returns before the window draws — on a cold browser that gap has been as long as ten seconds — so screenshot until the screen changes rather than concluding from one frame that nothing launched.

list_windowsA

The windows the window manager knows about — id, title, class, geometry, focus. A screenshot says what the desktop looks like; this says what any of it is, which is how you tell a browser that failed to open from one that has not painted yet. Match on class, not title: the class is the application, the title is whatever page it is showing. Linux only.

window_actionA

Focus, raise, minimize, maximize, unmaximize, close, move or resize one window. The reply is the window afterwards, not an acknowledgement — the window manager places the frame and applications snap to their own grid, so a move to 300,200 routinely lands at 305,229. Believe the response, not the request. On a Wayland desktop a tiled window cannot be moved or resized — float it first — and an action the compositor refuses is a 400. Prefer focus over raise: raising without focusing gives a window that is visibly in front and silently not receiving keystrokes. A 504 is neither a refusal nor a report that nothing happened unless its structured response explicitly says the request was not dispatched: an absent or ambiguous explanation leaves the action possibly already applied. An uncertain outcome is not permission to repeat it — the next call is list_windows, which says what the desktop is now, not this one again. Least of all for close, which cannot be undone.

read_clipboardA

What is on the computer's desktop clipboard right now — the CLIPBOARD selection, which is what Ctrl-C writes and Ctrl-V pastes. Use this rather than running xclip through exec: exec runs a login shell, so anything the guest user's profile prints lands on the same output ahead of your command's, which corrupts a read you are trying to parse. This does not share that stream. It requires a Linux desktop image with its desktop's clipboard tool installed — xclip on X11, wl-clipboard (wl-paste/wl-copy) on Wayland images such as Omarchy — and is refused on Windows. An image without that tool gets a 400 that names it and never clears; changing runtime state or retrying cannot fix that image dependency. It is a READ, not a subscription: this call notices nothing on its own, and repeating it is how a copy inside the guest is discovered. What does notice is wait_for_event on clipboard.changed, which fires when the selection changes hands and says which selection it was — the text is deliberately not on that stream, so this is still the call that fetches it. It also does not wake a suspended computer, so a stopped or suspended one is refused rather than started; start_computer first if you need it, unless a start is already under way — wait_for_computer tells you which. The 409s are not alike either, and the answer says which KIND it is rather than leaving you to match on the sentence: a computer that is not running does not clear by waiting UNLESS a start has already been admitted for it, in which case wait_for_computer is the call and start_computer would only start it twice — while a guest agent that is busy or still inside its boot window is worth asking again in a moment. A desktop session or X server that is not answering carries no such word, deliberately, because the platform cannot tell a guest still coming up from one nobody is logged into: bound your attempts there rather than looping on it. At most 128 KiB comes back, and more than that is refused rather than cut short.

write_clipboardA

Puts text on the computer’s desktop clipboard, ready to paste. This leaves it on the clipboard and touches nothing on screen — follow it with press_key and keys ["ctrl","v"] to get the text into whatever has focus. Those are two separate key NAMES in the array, not one string "ctrl+v", which press_key would reject as an unknown key. Use this rather than the setsid/xclip/base64 recipe through exec: it is one call, it is confirmed, and it cannot be broken by a quote in the text. It requires a Linux desktop image with its desktop’s clipboard tool installed — xclip on X11, wl-clipboard (wl-paste/wl-copy) on Wayland images such as Omarchy — and is refused on Windows. An image without that tool gets a 400 that names it and never clears; changing runtime state or retrying cannot fix that image dependency. Unlike read_clipboard this DRIVES the computer, so a suspended one is resumed to serve it and that resume is charged. At most 64 KiB of text goes in — half what comes out, because the text crosses to the guest inside a single command argument. Empty text ("") CLEARS the clipboard, and is the only way to: do it after pasting a secret, since otherwise the desktop keeps holding it. The platform confirms the write by reading the selection back before it answers, so a success here means the desktop is holding your text rather than that a command ran; a refusal saying the desktop did not take it means something else claimed the clipboard in that instant, and sending it again works. Not every refusal here is one of those, and the answer says which it is rather than leaving you to match on the sentence: a computer that is not running is a 409 that start_computer fixes and that retrying never will, while a guest agent still inside its boot window is worth another attempt. A desktop that is not answering is deliberately left unclassified — it can equally be a guest still coming up or a session nobody is logged into — so bound your attempts there instead of looping.

write_file_chunkA

Linux with python3 only. Stage up to 256 KiB in one chunk of a file up to 2 GiB. Supply raw bytes as base64, in order, keeping path, upload_id, size, overwrite and source contents unchanged. The final chunk publishes atomically. After any error call get_file_upload and continue from its offset; never guess or restart blindly. An empty chunk at offset=size retries publication. Call cancel_file_upload after completion or to abandon staging. Staging consumes guest disk until cancelled, with no expiry. May resume a suspended computer and incur charges. A 404 from an older host means resumable upload is not available on it: do not resend the chunk through write_file, which is neither atomic nor resumable. For large local files prefer the SDK or CLI, avoiding model-context transfers.

get_file_uploadA

Read the confirmed offset and completion receipt before retrying an interrupted chunk. Missing uploads return 404. May resume the computer unless no_wake is set.

cancel_file_uploadA

Remove an interrupted upload or completed upload receipt. Never deletes the published destination. Idempotent if staging is absent. May resume the computer unless no_wake is set.

write_fileA

Write a file inside the computer. Paths must be absolute — the guest agent inherits whatever working directory it was started in, so a relative path resolves somewhere you did not name. By default a file already at the path is replaced. Pass overwrite: false to create the file only if nothing is there: a path that is taken is refused and that attempt writes nothing — retrying does not change that. Incomplete contents are never published at the path, but a failure while publishing or answering can leave the COMPLETE file there: after any error, read the path before retrying or overwriting, since what is there may be your own upload. A refusal saying this computer’s host cannot do a create-only write, and a 503 on one, mean nothing was sent — but neither means the path is free. overwrite: false is for Linux computers; a Windows computer refuses it. A suspended computer is resumed to take the write, which is charged, unless no_wake is set.

read_fileA

Read a file from inside the computer. Text comes back as text and images come back as images; anything else comes back base64. A large file comes back a window at a time rather than filling the conversation: offset says where the window starts, and the note under a truncated read gives the exact offset to pass next. There is no size a file can be that makes it unreadable this way — a 2 GB log is pages, not a refusal — but a file you want whole is still better pushed out of the guest than carried through a conversation, and the note says how. This reaches the guest agent to do it, so a suspended computer is resumed to answer and that resume is charged — it can even come back 402 — unless no_wake is set, which refuses instead. read_clipboard, by contrast, refuses a suspended computer rather than starting it.

get_executionA

Read one background execution by stable execution_id. Last-observed running, exited or lost; running is not proof the computer is awake, and lost establishes no success or failure. Metadata only: no guest I/O, activity refresh, resume, polling loop, PID fallback or command replay. Handles are volatile and can become unavailable after restart, replacement, expiry or cleanup.

read_execution_outputA

Read volatile, mutable guest output once at explicit independent stdout_offset and stderr_offset byte positions. Performs guest I/O but does not refresh activity, resume, change shared PID cursors, retry, tail, capture or replay a command. Unsuitable for passive Activities/history. Each stream is limited to 4096 bytes by default, at most 16384. Text is lossless UTF-8 (BOM preserved); NUL, binary and split UTF-8 remain exact base64. False more flags mean current EOF, not completion. Diagnostics repeat independently; only the first 4096 diagnostic bytes are displayed, with distinct available/displayed counts and display/daemon truncation flags.

retain_execution_outputA

Capture one immutable prefix version from volatile guest output by stable execution_id. Explicit guest I/O; never resumes, retries or replays a command. Each call creates a separate version. Capture may commit even if its response is lost. Default 1 MiB per stream/max 4 MiB and default 86400s/max 604800s retention; diagnostics separately capped 64 KiB.

get_resultA

Read one immutable result manifest without guest output/files, resume, capture or command replay. Expiry, current scope or storage availability can make a version unavailable.

read_result_outputA

Read one passive retained stdout, stderr or diagnostic page at an explicit offset. Default 4096/max 16384 bytes. EOF concerns this immutable prefix, not task completion. Lossless UTF-8/BOM or exact base64; no guest read, shared cursor, implicit capture, iteration or replay.

delete_resultA

Delete exactly this immutable result version. No metadata preflight or guest mutation. Same deletion effect on repeat, but a repeated 404 remains unavailable; no automatic retry.

publish_artifactA

Explicitly capture a caller-nominated absolute Linux, Windows drive or UNC file path with expected_size and lowercase expected_sha256 already supplied. One guest file read, no preflight stat/list/hash/exec. Optional execution_id is verified caller-selected association, not proof of authorship. Default 8 MiB/max 64 MiB capture cap; default 86400s/max 604800s retention. New version per call; a lost response leaves publication unconfirmed.

get_artifactC

Passive finite metadata for one retained artifact. No guest file or execution lookup, resume, capture or fallback.

read_artifactA

Read metadata, then only if the complete artifact fits max_bytes (default 4096/max 16384), read the whole immutable object and verify size/SHA-256. Over cap returns metadata only, with zero content GETs. One 90s operation budget; no Range, prefix, assembly, file destination, guest fallback or image/HTML rendering. Lossless UTF-8/BOM or exact base64.

delete_artifactB

Delete exactly this retained artifact, without metadata preflight or guest file deletion. Same deletion effect on repeat; repeated 404 remains unavailable. No retry.

list_snapshotsA

Every snapshot on this account. orphaned means its computer is gone: such a snapshot can still be cloned into a new computer, but cannot be restored, because a restore puts the disk back on a source that no longer exists. restore_available: false means the only usable copy is on a host its computer has since left: restore_snapshot will not work on it, though it can still be cloned. Read state ON EVERY ROW before acting, rather than on the newest one — this is one answer per hypervisor concatenated in a fixed host order that has nothing to do with time, so a capture running on one host routinely appears after finished snapshots from another. A row reading capturing is not a snapshot yet: the copy is still being taken and restore, clone and delete all fail on it. It carries the id the finished snapshot will keep, so this is also the route to poll after create_snapshot: the row stops reading capturing in place rather than being replaced under another id, and a row that vanishes without ever leaving capturing is a capture that failed. pending is the point at which it can be acted on, and durable means it has reached backup storage as well.

snapshot_holdingsA

How many snapshots a computer has, what they weigh, and the fingerprint that names that exact set. Read this before purging snapshots with delete_computer: the fingerprint is what binds the purge to the snapshots you were shown, so one that arrived after you looked cannot be swept up in it.

create_snapshotA

Capture a computer so it can be restored or forked later. A disk snapshot is the filesystem; a memory snapshot also saves the running session, so a fork of it comes up with the same processes and windows already open. Name it after the step it is about — that name is what picks it out of the list later. THE CAPTURE OUTLIVES THE REQUEST that starts it: the platform accepts it and copies the disk afterwards, which takes minutes and scales with how much has been written. This waits for the copy to land by default and answers with the finished snapshot; pass wait: false to get the id straight back and poll list_snapshots yourself. Everything that can refuse a capture — no such computer, one already running, a memory snapshot of a computer that is not running, an allowance that will not stretch — is refused by this call, so anything else is a capture that started. A wait reports progress while it runs, so a client that sends a progressToken and sets resetTimeoutOnProgress can hold the request open; a client that cannot should pass wait: false and poll list_snapshots, rather than watch its own default timeout cancel a call while the capture goes on running.

restore_snapshotA

Put a snapshot back onto the computer it came from, discarding everything on that disk since. Check restore_available in list_snapshots first: where it is false, the snapshot's usable copy is on a host its computer has left, and a restore is refused while clone_snapshot still works. Refused on an orphaned snapshot — clone_snapshot is what works there. It leaves the computer RUNNING whatever state it was in: restoring a stopped one boots it, which is a start like any other and is charged. A disk snapshot comes back to a fresh boot, a memory one to the captured session, and either way a suspended session the computer was holding is discarded with the disk it was saved against.

snapshot_scheduleA

When the platform takes this computer's automatic snapshot. Reading takes no arguments; setting takes an hour. There is deliberately no "last run" here — snapshots carry real capture times, and that is what a freshness check should read. This says when they are TAKEN and not how long they survive: get_retention is the other half, and it is what tells you whether the snapshots this schedule takes will still be there next month.

get_retentionA

The plan's retention window — the other half of snapshot_schedule, which says when snapshots are taken and deliberately has no field for how long they survive. Read it before promising anyone that a backup will still be there, and before taking a snapshot you mean to keep. Takes no arguments: the window belongs to the ACCOUNT, not to a computer, though each computer keeps its own set under it. ONLY AUTOMATIC SNAPSHOTS ARE AGED OUT — one you took yourself with create_snapshot is never removed automatically, which is how you keep something past the window.

clone_snapshotA

Build a new computer from a snapshot, leaving the original untouched. This is the fork half of snapshot-and-fork, and the only thing that works on an orphaned snapshot. A MEMORY snapshot is resumed: the copy comes up running the captured session, and is given its own MAC, address, hostname, machine ID, SSH host keys and desktop password before its network comes up, so it runs beside its source; if that fails it is left stopped, and starting it cold-boots its disk. Pass memory: false to build it from the disk alone instead. A clone built from a DISK — a disk snapshot, memory: false, or a memory snapshot the platform did not resume — comes back building and ends its build STOPPED: start it with start_computer once the build finishes (wait_for_operation on its operation). A memory snapshot of a computer that held SECRETS is resumed only with inherit_secrets: true, and then the copy HOLDS THE SAME CREDENTIALS as the computer it came from; without it the clone is built from the disk and this tool says so.

delete_snapshotA

Remove a snapshot permanently. Later snapshots in the same chain are unaffected. THE DELETION OUTLIVES THE REQUEST that starts it: the platform accepts it and then detaches the dependent snapshots and removes the stored objects, which takes time that scales with the chain and with how much is stored. This waits for it by default and reports what actually happened; pass wait: false to hand back as soon as it is accepted. A 409 saying the snapshot is ALREADY BEING DELETED is progress rather than a fault — the platform is doing what you asked, and the answer is to watch that one finish, never to go and delete something else. A wait reports progress while it runs, so a client that sends a progressToken and sets resetTimeoutOnProgress can hold the request open; a client that cannot should pass wait: false and poll list_snapshots.

get_template_schemaA

The JSON Schema for a mandala/v1 template document — the declarative form a template is written in. Read this before writing one: it describes the ref, the image family, what the template is layered onto, and the shape a computer gets when a create names no numbers.

check_templateA

Check a document against the SCHEMA, without publishing it. Nothing is stored and no ref is claimed, so this is safe on a draft and safe to call repeatedly, and every problem comes back at once. Always check before publishing: a ref is immutable, so a document published with a mistake in it cannot be corrected under that version. WHAT IT CANNOT SEE: this reads the document alone and knows nothing about your account, so valid: true does not mean publish_template will succeed. Whether the namespace is yours, whether the family is yours, whether your plan may publish at all, and whether the ref is already taken or retired are all decided at publish time and can still refuse a document that checks out here — among them a spec.hardware.disk_gb below the disk floor of its family or parent template (400), and a document that declares a build into a family that is not your account's (403). It DOES report that spec.build and spec.env need a parent in spec.from, and that a document with build steps may only layer onto a system/... template (anything else is a 400). It does NOT check spec.secrets beyond its shape: the cap of 32 references, and whether each id is a secret in your scope, are decided when build_template is called.

publish_templateA

Store a document under a ref of your own, so create_computer can launch it by name. THE NAMESPACE IS YOUR ACCOUNT: metadata.namespace has to be your account id, and anything else is refused rather than rewritten — system included. A REF IS IMMUTABLE: publishing the identical document again succeeds and changes nothing, but publishing a DIFFERENT document under the same ref is refused, and the fix is to bump metadata.version. What counts as different is the digest, so a changed label is a change. Check the document first — a mistake published under a version can never be corrected under that version. NEEDS AN ACCOUNT-WIDE KEY: templates are the account's, so a key confined to a workspace is refused (403) whatever it sends; such a key can still list_templates and launch a template by its ref.

get_templateA

One template as the document it was written as — the lineage, the build steps and the digest, which list_templates drops. Works for your own namespace and for system, so you can read what you are layering onto before writing a document of your own. Without version this is the newest, which is also what a create naming the unpinned namespace/name resolves to. Your account's own namespace needs an account-wide key: a key confined to a workspace is refused (403) there, because the document is the account's and may carry its build steps and scripts; it can still read system templates.

retire_templateA

Stop a template resolving, and give its row back against your ceiling. WITHOUT version THIS RETIRES EVERY VERSION OF THE NAME — that is what retiring a template means here, and it is deliberately not get_template's "the newest". Pass version to take exactly one. THIS CANNOT BE UNDONE: a retired ref is refused for ever, identical bytes included, so the version you retire can never be published again — publish the next version instead. Computers are NOT affected: a computer is built from the image the ref resolved to and holds no reference to the document, so anything already running, stopped or suspended keeps working. What a retire breaks is resolution — a NEW create naming the ref is refused. Needs an account-wide key: a key confined to a workspace is refused (403) whatever it names. Say what it costs before you call it.

build_templateA

Compile a document that declares spec.build steps into a golden image. Returns IMMEDIATELY with a job — a build takes minutes, an agent image roughly fifteen — and watch_build is how you follow it. THE NAMESPACE AND THE FAMILY BOTH HAVE TO BE YOURS: spec.family is what the image is called on a hypervisor, in a directory shared with every computer on that machine, so a build may only write into golden-<your account id> or that and a - and a name of your choosing. A document with build steps must LAYER ONTO ONE OF OURS: spec.from has to name a system/... template (and spec.build or spec.env need a parent at all); anything else is a 400. spec.secrets names secrets by ID (csec-…) and the environment variable each appears as, never by value, and at most 32 of them — check_template does not check that cap. They resolve among the account-wide secrets. Its refusals: a 400 saying the document names a secret that does not exist is deliberately the same for a deleted secret, a missing one, a workspace's and another account's, and is not worth retrying, nor is a 400 for a malformed reference or more than 32; a 409 saying a secret's value cannot be read right now, and a 503, are worth retrying. A refusal saying a host is busy is not a problem with your document — one build runs per hypervisor — and is worth retrying. After a successful build, pass the published ref to create_computer as template. create_computer may still return image preparation instructions or a capacity refusal; follow that result rather than assuming the successful build made every destination ready. ACCOUNT-WIDE KEYS ONLY: builds are the account's, so a key confined to a workspace is refused (403) here and by list_builds, get_build and watch_build; such a key launches a published template by its ref instead.

list_buildsA

Every build this account has started that the fleet still holds a record of, newest first. A build lives on the hypervisor that ran it, so this asks all of them — and WITHOUT allow_partial, one that cannot be reached makes the platform refuse rather than answer short, so an empty or small list is the truth rather than an outage. Pass allow_partial and that stops being so: you get the short answer, opening with an INCOMPLETE line, and nothing else in the result says how much is missing. Needs an account-wide key: a key confined to a workspace is refused (403).

get_buildA

What became of one build, which template it was for, and which step it is on. Reads once and returns; watch_build is what follows a running one. It stays readable after the build has finished, so this is also how you find out which step failed on a build nobody was watching. Needs an account-wide key: a key confined to a workspace is refused (403).

watch_buildA

Follow a build to its end and report what happened. Streams the platform's own progress — each event is sent only when something actually moved — and logs each one as it arrives, so a long build is visibly alive rather than indistinguishable from a hang. A build that FAILED is a normal answer here, not an error: read status and the failed step. Attaching to a build that has already finished is fine and returns immediately. Needs an account-wide key: a key confined to a workspace is refused (403).

wait_for_eventA

Block until the computer reports something, instead of screenshotting in a loop to find out whether it has. This is the tool that replaces polling: a window opening, a background command exiting, the desktop coming up, the machine going idle or changing power state. Types are window.opened, window.closed, window.focused, window.blurred, clipboard.changed, file.changed, process.exited, computer.ready, computer.idle, computer.started, computer.stopped, computer.suspended, and the list grows — pass none to wait for the next thing of any kind. It returns everything that happened up to and including the match, in order, so the answer is what the computer did rather than one fact out of it. Nothing is lost between calls: this server holds the stream open across turns, so a wait that times out has missed nothing and calling again picks up exactly where it left off. That is why the timeout is short — waiting again is free. Do not use it to wait for a click to land or a page to paint: neither is an event, and a screenshot is still how you find out what the screen looks like.

wait_for_file_changeA

Block until something is created, changed or deleted anywhere under a directory in the guest, instead of running ls in a loop to find out whether it has. This is how you wait for a build to write its output, a download to land, or a script to produce a file. It nominates the directory on this computer's event stream, waits until the guest is genuinely watching it, and only then waits for a change — so a timeout from this tool means nothing changed, never "nothing was watching yet". The nomination sticks: up to 4 trees stay watched across your turns, so a second call on the same path is instant and a change that happens between two of your turns is still waiting for you. Nominate the NARROWEST directory you care about. A home directory under a build is thousands of changes a second, and what you get back for one of those is a single "too much changed" marker rather than the events. It reports changes, not contents: read_file and exec are still how you find out what is in a file. A rename inside the tree arrives as a delete and a create, and nothing is announced about what was already there when you nominated it — list the directory for that.

poll_eventsA

Everything the computer has reported that you have not been handed yet, without waiting. This server holds the event stream open between your turns, so this drains what accumulated while you were doing something else — including while you were running other tools on the same machine. Use it after a long exec, or whenever you want to know what changed without spending a screenshot. Returns immediately, and an empty list genuinely means nothing has happened.

list_webhooksA

Every webhook subscription on the account, with its health: whether it is enabled, when its endpoint last accepted a delivery and when one last failed. A webhook is an HTTPS endpoint the platform POSTs events to — the other transport for the same events wait_for_event reads, for a CI job or a queue worker that wants to be woken rather than to wait. Never carries a secret: that is shown once, by create_webhook or rotate_webhook_secret. A workspace-scoped key sees only the subscriptions confined to that workspace.

create_webhookA

Subscribe an HTTPS endpoint to this account’s events. THE ANSWER CARRIES THE SIGNING SECRET ONCE — it is not readable again, so store it or hand it over before doing anything else. Each event is POSTed as the same JSON object wait_for_event hands you, byte for byte, signed with Standard Webhooks v1 headers (webhook-id, webhook-timestamp, webhook-signature); the receiver should verify the raw body, acknowledge with a 2xx before doing the work, and refuse a timestamp more than 300 seconds off. A failing delivery gets eight attempts (seven retries) over about fourteen hours; an endpoint that keeps failing for a day is disabled. The url must be https, at most 2048 characters, must not carry a username or password, and must resolve to a public address — a private or loopback address is refused. Paid plans allow ten subscriptions; an account with no plan allows none.

get_webhookA

One subscription with its health: whether it is enabled and why not if not, when the endpoint last accepted a delivery, when one last failed, and the status of the newest attempt. Never the secret. For the individual deliveries and what became of each, list_webhook_deliveries.

update_webhookA

Change the endpoint, the description, the filters, or whether it is enabled. Fields you leave out are kept as they are; name at least one. A new url is checked exactly as on create. enabled: true clears a disable the platform imposed for failures and starts fresh; enabled: false stops deliveries and records that you chose to. While the account’s plan includes no webhooks, enabled: true (and a new url) is refused with 402. The secret does not change — rotate_webhook_secret is for that.

rotate_webhook_secretA

Mint a new signing secret for a subscription. THE ANSWER CARRIES IT ONCE, like create_webhook. The old secret goes on being honoured for 24 hours — every delivery in that window carries two signatures on the one webhook-signature header, new first, so a receiver that accepts either passes throughout — which is the window to get the new one deployed. Rotating again inside it replaces the previous secret rather than keeping three. Do this when a secret has leaked or when whoever held it has moved on.

test_webhookA

Queue one signed delivery of a synthetic webhook.test event through the ordinary path, so it is signed, retried and recorded exactly as a real one. The answer is the delivery record, ACCEPTED rather than finished: the endpoint has not been called yet when this returns. Read what it said back with list_webhook_deliveries a little later — a first attempt goes out within seconds, and a failed one is retried after 30 seconds. A disabled subscription is refused; enable it first with update_webhook.

list_webhook_deliveriesA

The newest hundred deliveries to one subscription, newest first, each with its state (pending, in_flight, delivered, exhausted or dropped), its attempt count, and the HTTP status or one-line error of its newest attempt. This is where a delivery that ran out of attempts shows up — nothing is dropped silently. Finished deliveries are kept until seven days after they were queued; pending ones until they finish. The one-line error is an open set — timeout, dns, refused, reset, unreachable, tls, redirect, address refused, status NNN and others — and a dropped delivery says why it was dropped. Each carries the event’s cursor, which is what to pass as since to the socket to read on from that point.

delete_webhookA

Remove a subscription and every delivery record it holds, pending ones included. Nothing more is sent to the endpoint. Irreversible: a new subscription to the same url gets a new id and a new secret. To stop deliveries without losing the record, update_webhook with enabled: false instead.

list_ssh_keysA

The SSH public keys registered to the person this API key belongs to — the same list whichever account the key acts on. A key added from the dashboard is accepted by every computer with SSH on, on every account where that person is an owner or member; one added through an API key or connected app (add_ssh_key) only on the account that credential acts on, until the credential is revoked. Shows id, type, SHA256 fingerprint, name and reach: every account, this account only, or another account only (refused on this account's computers; which account is not said).

add_ssh_keyA

Register an OpenSSH public key to the person this API key belongs to. It is accepted only by computers with SSH on in the account this credential acts on, and is removed when the credential is revoked. Accepted: Ed25519, ECDSA (P-256, P-384, P-521), their security-key forms, and RSA of at least 3072 bits. Refused: DSA, shorter RSA, certificates, a line with SSH options in front of the key, and anything but one key on one line. A key can belong to one person only, and each person may hold eight. Never send a PRIVATE key.

remove_ssh_keyA

Remove one of your SSH keys. New connections with it are refused at once, and it is removed from the computers it was written into within moments, and any session opened with it is closed as each computer receives the new key list. Only a key bound to the account this API key acts on, one list_ssh_keys marks "this account only", can be removed here. Any other key, one added from the dashboard ("every account") or one bound to another account ("another account only"), answers as if there were no such key, and is removed from the dashboard.

get_computer_sshA

Whether SSH is switched on for a computer, whether that computer can run SSH at all (one made from an older template image cannot), how many keys can log in, whether its hypervisor has the current setting yet, and whether that hypervisor refused the setting (it is not sent again until a key or the setting changes). Any role may read it.

set_computer_sshA

Turn SSH on or off for a computer. On, the computer runs an SSH server reachable only through the platform jump host, accepting the keys of every owner and member of the account, except keys added through an API key or connected app on another account, and keys of a member whose seat is suspended; off, the server stops and open SSH sessions are closed. No restart either way. Owners and members only.

list_public_urlsA

The public URLs of one computer, oldest first: each is https://<label>.<base_domain> serving a port inside the computer, public (anyone with the link) or private (an API key of this account). Also says whether they can answer here at all — enabled is false, with a reason, where public URLs are off or the account’s plan includes none, and the URLs are listed either way so you can delete them — and how many this computer may have. If the plan drops below the number a computer has, the oldest within the limit keep answering and the rest answer 404.

create_public_urlA

Serve a port inside the computer at https://<label>.<base_domain> (e.g. https://brave-otter-3f2a.mandala.dev), on 443 with no port in the address. PUBLIC BY DEFAULT: anyone on the internet with the link can open it — pass access: "private" to require an API key of this account instead. The app must listen on 0.0.0.0:, not 127.0.0.1. Without a label one is picked. A label is 3 to 40 letters, digits and hyphens, and the platform refuses names that read as its own or another company’s (www, login, brand names and their look-alikes) with a 400 that says why. Other refusals: 402 when the plan includes no public URLs; 403 PUBLIC_URLS_BLOCKED when the platform has turned public URLs off for this computer or account (not a role, and not something to retry — tell the user); 404 PUBLIC_URLS_DISABLED when public URLs are not enabled here at all (not a missing computer); 409 LABEL_TAKEN when the label is taken — a label is never given to a second account, so one another account has ever held, or one the platform has blocked, reads as taken — and 409 PUBLIC_URL_LIMIT when the computer has as many as its plan allows (Solo 2, Studio 5, Swarm 10); neither 409 clears by waiting. 429 after too many in a day, with Retry-After. Responses are marked noindex, cookies the app sets stay on its own host name, and a Mandala API key sent to a public URL never reaches the app. Not idempotent: a retry after a lost answer makes a second URL — list_public_urls first.

update_public_urlA

Point a public URL at another port, switch it between public and private, or both. The label is the URL and does not change — delete it and create another for a new name. Fields you leave out are kept; name at least one.

delete_public_urlA

The URL stops answering at once. Its label stays with this account: no other account can ever take it, and this account may use it again (create_public_url with that label) unless the platform has blocked it. The app inside the computer keeps running. A URL made again is a new id.

get_computer_secretsA

Which of the account's secrets a computer is bound to: for each, the secret id, the revision last delivered, and where the guest finds it — an environment variable name, or a file under /run/mandala-secrets/user/files. Values are NEVER shown — no value crosses this route. Also answers version, the number to send to set_computer_secrets so it changes only this list (0 for a computer never bound). Any role may read it. The list is what the computer receives at its next start or restart, which a running computer may not have yet. Every start and restart delivers each secret's latest value. Separately, when a secret's value is replaced on a running computer: a secret bound as a FILE has that file rewritten in place within seconds; one bound as an ENVIRONMENT VARIABLE, on an image that supports it, reaches new shells within seconds, and every exec — plain or with desktop: true — runs with the bound variables as they are at that moment, so the next exec sees the new value; programs already running — the desktop session among them — keep the value they started with until a restart (older images wait for the restart). A file newly bound or renamed waits for the restart either way.

set_computer_secretsA

Replace the WHOLE list of secrets a computer is bound to — not a merge: any binding left out is removed, and secrets: [] removes every binding. Each entry names a secret id and exactly one of env (the environment variable it is delivered under) or file (delivered as /run/mandala-secrets/user/files/) — at most 32 secrets, 8 of them as files, none twice and no variable or file name twice — and is recorded at that secret's latest revision unless it names revision_id, which must be the revision the computer holds now (any other revision is refused). A binding's revision is what the computer last received, never a pin: naming it only says this change keeps the value the running computer already has, so the change does not stop it being suspended. Only ids, revisions and names are sent and answered — values are NEVER shown or set here. The new bindings reach the guest at its NEXT START OR RESTART: a running computer keeps what it booted with until then, and a suspended one resumes with its old values. Every start and restart delivers each secret's latest value. Separately, when a secret's value is replaced on a running computer: a secret bound as a FILE has that file rewritten in place within seconds; one bound as an ENVIRONMENT VARIABLE, on an image that supports it, reaches new shells within seconds, and every exec — plain or with desktop: true — runs with the bound variables as they are at that moment, so the next exec sees the new value; programs already running — the desktop session among them — keep the value they started with until a restart (older images wait for the restart). A file newly bound or renamed waits for the restart either way. Binding a computer that has NO secrets yet requires it to be STOPPED: that first binding is refused with 409 while the computer is running or suspended, because a computer started without secrets cannot receive them until it starts again, and a restart does not do that. Start it afterwards to deliver them. Send version from get_computer_secrets to change only the list you read: if it changed since, this is refused with 409 and nothing changes. Also 409 while a delivery to the computer is in progress. After a change that drops a secret or moves one to another revision, a running computer cannot be suspended or memory-snapshotted until it restarts. Owners and members only; Linux computers only, and binding one for the first time needs a template whose image can receive secrets.

list_secretsA

The secrets stored on this account, in one scope: the account-wide ones by default, or one workspace's with workspace_id. For each: its name, its id (what set_computer_secrets binds), its current revision_id (what replace_secret and delete_secret need), its scope and when it was last delivered to a computer. Values are NEVER shown — no route returns one. Also says whether binding secrets to computers is switched on here, and the store's limits. Owners and members may read; a viewer is refused. An API key confined to a workspace lists that workspace's secrets, and naming another scope is refused.

get_secretA

One secret's name, scope, current revision_id and when it was last delivered — never its value. Read it for the current revision_id after replace_secret or delete_secret is refused because the revision moved. A secret in a workspace is found only with its workspace_id; otherwise it is not found.

create_secretA

Store a value under a name on this account, encrypted, for binding to computers with set_computer_secrets (or the secrets field of a create). The value is sent once and NEVER shown again: this answer, every other tool and every route give back only the name, id and revision. Do not put the value anywhere else — not in a command line, a file or a message — when a binding can deliver it. Owners only. A name is unique within its scope (up to 60 characters, no control characters); a taken name is refused with 409. The account holds at most 100 secrets at once and may create at most 1000 over its lifetime — list_secrets reports both. Without workspace_id the secret is account-wide and any computer on the account may be bound to it. NOT safe to repeat blind: if the answer is lost, list_secrets before creating again, since the secret may exist.

set_secretA

Make a name hold a value: create the secret if the scope has no secret by that name, or replace its value if it has — the one to reach for when either will do. It reads the scope first and replaces against the revision it read; names match ignoring ASCII case, as the platform keeps them unique. If the secret is created or replaced by someone else between the read and the write, it reads again and tries again, up to 3 times. The value is NEVER shown back. Owners only. A new name counts against the account's limits on secrets held and created (list_secrets reports both). Every start and restart delivers each secret's latest value. Separately, when a secret's value is replaced on a running computer: a secret bound as a FILE has that file rewritten in place within seconds; one bound as an ENVIRONMENT VARIABLE, on an image that supports it, reaches new shells within seconds, and every exec — plain or with desktop: true — runs with the bound variables as they are at that moment, so the next exec sees the new value; programs already running — the desktop session among them — keep the value they started with until a restart (older images wait for the restart). A file newly bound or renamed waits for the restart either way. NOT safe to repeat blind when the answer is lost: list_secrets says whether the change landed.

replace_secretA

Replace a stored secret's value, and move its revision_id. Send the revision_id list_secrets or get_secret answered: if it has moved since, this is refused with 409 and nothing changed — read it again, and replace again only if you still mean to. The new value is NEVER shown back. Owners only. Every start and restart delivers each secret's latest value. Separately, when a secret's value is replaced on a running computer: a secret bound as a FILE has that file rewritten in place within seconds; one bound as an ENVIRONMENT VARIABLE, on an image that supports it, reaches new shells within seconds, and every exec — plain or with desktop: true — runs with the bound variables as they are at that moment, so the next exec sees the new value; programs already running — the desktop session among them — keep the value they started with until a restart (older images wait for the restart). A file newly bound or renamed waits for the restart either way. That live send is asynchronous and best effort: this answer does not wait for it or say whether it landed, and a running computer may still hold the old value until its next start or restart, which always delivers the latest. get_computer's secrets_pending says whether one is still missing the change.

delete_secretA

Permanently delete a stored secret. This cannot be undone, and it is NOT refused while computers are bound to it: such a computer keeps the value it already holds, but can never be given its secrets again — its next start is refused (409), a restart or a reboot inside it stops it, and a suspended one cannot resume. Remove the binding with set_computer_secrets first (or on the stopped computer afterwards) so it can start. Deleting does not recall a value already delivered. Needs the current revision_id (a stale one is refused and nothing is deleted) and confirm: true. Owners only. Deleting frees a place under the account's limit on secrets held at once, but not under its lifetime limit on secrets created.

get_creditsA

This account's Mandala AI credits, which pay for run_agent when no model key is configured: available (balance minus debt and credits held for runs in progress; can be negative, and zero while frozen), balance, held, debt, frozen (no plan or a suspended account: credits are kept and still expire, but cannot be spent), this billing period's plan grant, the unexpired lots, and the journal, newest first. Amounts are integer USD micros (1,000,000 = $1). A key confined to a workspace is refused (403): the ledger is the whole account's.

get_agent_runA

A background agent run as it stands: its status, every step so far, its cost on AI credits, and once it has ended, how. Poll it every few seconds while its status is running; a step takes at least that long, so polling faster only spends rate budget. Only status completed with stop end_turn means the task was finished. completed with any other stop (max_steps, max_cost, credits_exhausted, rate_limited, refusal, …) stopped without finishing; failed carries the error that ended it; cancelled was stopped by cancel_agent_run; interrupted means the platform restarted while it was going, or its ending was not recorded, and it was not resumed — it MAY HAVE DONE MORE THAN IT RECORDED. Whatever the ending, the steps a run took stand: take a screenshot before deciding what is left, and continue with its thread_id rather than running the whole task again. A run this key cannot see is not found.

list_agent_runsA

Background agent runs on this account, newest first, a page at a time: each run's id, computer, status (running, completed, failed, cancelled or interrupted), how it stopped, its steps and cost. Only completed with stop end_turn is finished. computer_id keeps one computer's runs — it is not defaulted to the selected computer — and status keeps one status: status running finds a run still going, which is the one to wait for or cancel before starting another on that computer. Steps and closing text are not listed, and long prompts are shortened; get_agent_run reads one whole. Pass before with the last run's id for the next page. A key confined to a workspace sees that workspace's runs. The account keeps its 1000 newest ended runs and every run still going.

cancel_agent_runA

Stop a background agent run where it is: the model call in flight is abandoned and no further action is taken. What it already did to the desktop stands, and so does what it cost: take a screenshot before deciding what is left. Cancelling a run that has already ended changes nothing and answers it as it is. A run this key cannot see is not found.

list_agent_threadsA

The conversations agent runs have kept on this account, most recently used first: each thread's id, the computer it belongs to, a title (the first line of its first prompt) and how many turns it holds. Runs from run_agent, run_agent_chat and start_agent_run are kept unless they sent store: false. To continue one, send its id as thread_id on the next run, on the same computer. computer_id keeps one computer's threads — it is not defaulted to the selected computer. Pass before with the last thread's id for the next page. A key confined to a workspace sees that workspace's threads; viewers are refused, since a prompt is where private text ends up.

get_agent_threadA

One kept conversation, oldest turn first: each user turn is a prompt, and each assistant turn is how that run answered — its closing text, how it ended, its steps, and its cost on AI credits. Read it to see what earlier runs on a computer were asked and did before continuing it with thread_id. An answer holds the newest turns that fit in about 48,000 characters; when earlier turns did not fit it says so, and before_turn reads the ones before. Only an assistant turn that ended end_turn finished its task. One that is running (its run is still going), aborted (stopped part way) or interrupted (the platform restarted mid-run) MAY HAVE DONE MORE THAN IT REPORTED: look at the desktop before relying on it. A thread this key cannot see is not found.

delete_agent_threadA

Permanently delete a kept conversation: its turns are gone and its id can no longer be continued. Nothing else changes — no run is stopped (cancel_agent_run stops a background run), and what its runs did to the desktop stands. Delete only a thread this conversation started or one the user asked to remove: every thread on the account is visible to this key, including other people's. It is for removing a prompt that held private text, or making room: the account keeps 1000 threads, replacing the least recently used, and a key that can delete none of them gets runs that are not kept. Needs confirm: true.

list_directoryA

Read one passive, unordered directory listing from an already running computer. No resume or idle extension; ordinary API rate/capacity admission still applies. At most 512 examined entries/128 KiB, with no continuation token. Narrow the path when partial. Symlinks are not followed; a final symlink directory is refused. No file content is read.

list_activitiesB

Selected, retained, best-effort request history; not all guest work or an agent identity. Accepted background work is not completed execution; a dispatched error may have had effects. Activity IDs identify requests, never idempotency keys. History is newest first with a fixed watermark. Changes include late final updates to older rows. No automatic paging. A gap requires refreshing history.

get_activityB

Selected, retained, best-effort request history; not all guest work or an agent identity. Accepted background work is not completed execution; a dispatched error may have had effects. Activity IDs identify requests, never idempotency keys.

get_activity_resultsB

Metadata only: at most eight newest links. more has no cursor and does not mean all versions were returned. No content reads, guest files, captures, execution polling or downloads. An unavailable link is not empty content. Caller-selected artifact association does not prove creation or command success.

read_signalsA

Read finite daemon facts without waking the computer, guest contact, event socket, watcher, capture or polling loop. Omit since or send empty for a head-only baseline with no replay; neither a baseline nor gap proves no earlier activity or task success. Keep the returned cursor even on an empty page: filtered-out rows can advance it. Retention is ephemeral, and resets are explicit gaps.

Prompts

Interactive templates invoked by user choice

NameDescription

No prompts

Resources

Contextual data attached and managed by the client

NameDescription

No resources

TDQS

B3.4/5.0

Scored across 122 tools

Disambiguation2/5

While many lifecycle and desktop tools are distinct, there is a large cluster of subtly overlapping tools whose boundaries are hard to keep straight: read_file vs read_artifact vs read_result_output vs read_execution_output vs get_result, and get_execution vs read_execution_output vs retain_execution_output vs get_artifact/publish_artifact. Similarly set_secret/create_secret/replace_secret all mutate secrets, and move_computer vs move_computer_to_workspace sound near-identical. The verbose descriptions try to disambiguate but the sheer functional overlap means an agent can easily misselect.

Naming Consistency4/5

The set overwhelmingly follows a snake_case verb_noun pattern (list_workspaces, create_snapshot, delete_secret, wait_for_event), which is predictable and readable. Minor deviations exist for imperative desktop/shell primitives (exec, wait, click, drag, scroll, whoami) that drop the verb_noun shape, but even these are consistent snake_case.

Tool Count1/5

122 tools is far beyond any reasonable bound, even for a broad cloud-desktop platform; the rubric treats 50+ as an extreme mismatch. Many tools (results, artifacts, executions, activities, signals) exist as narrow variants that could be consolidated into a handful of parameterized operations.

Completeness4/5

Coverage is remarkably broad: computer lifecycle, snapshots, templates/builds, secrets, webhooks, SSH, public URLs, files, exec, desktop input, workspaces, agent runs, artifacts and activities. Minor gaps remain (no agent-run start/run tool is listed though descriptions reference run_agent/start_agent_run, and API key minting/revocation is deliberately excluded), but core workflows have no obvious dead ends.

Maintenance

ActivityActive
ResponsivenessNo issues