Mandala Computer
OfficialServer Configuration
Describes the environment variables required to run the server.
| Name | Required | Description | Default |
|---|---|---|---|
| MANDALA_TAGS | No | Comma-separated tool tags to keep; see Tool filters in the README. | |
| MANDALA_API_KEY | No | API key from Settings, API keys. Optional after mandala login, which saves one. | |
| MANDALA_PROFILE | No | Saved profile to use; --profile overrides it. | |
| MANDALA_BASE_URL | No | API base URL. With a saved profile, must match its stored base. | https://app.mandala.computer/api/v1 |
| MANDALA_MODEL_KEY | No | An 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_ONLY | No | 1 or true keeps only read-only tools. | |
| MANDALA_COMPUTER_ID | No | Bind a computer at startup, so use_computer is not needed. | |
| MANDALA_NO_LIFECYCLE | No | 1 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
| Capability | Details |
|---|---|
| tools | {
"listChanged": true
} |
| logging | {} |
Tools
Functions exposed to the LLM to take actions
| Name | Description |
|---|---|
| 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, |
| 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 |
| 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 |
| 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. |
| 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. |
| 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 |
| 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. |
| 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 |
| 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 |
| 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: |
| 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. |
| 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 |
| 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_retentionA | The plan's retention window — the other half of |
| 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 |
| 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 |
| publish_templateA | Store a document under a ref of your own, so create_computer can launch it by name. THE NAMESPACE IS YOUR ACCOUNT: |
| 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 |
| retire_templateA | Stop a template resolving, and give its row back against your ceiling. WITHOUT |
| build_templateA | Compile a document that declares |
| 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 |
| 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 — |
| 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 |
| create_public_urlA | Serve a port inside the computer at |
| 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 |
| set_computer_secretsA | Replace the WHOLE list of secrets a computer is bound to — not a merge: any binding left out is removed, and |
| 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
| Name | Description |
|---|---|
No prompts | |
Resources
Contextual data attached and managed by the client
| Name | Description |
|---|---|
No resources | |
TDQS
Scored across 122 tools
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.
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.
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.
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.