• English
  • Computer Use

    computer-use is a worker process built to drive a real desktop:

    • Mouse and keyboard
    • Screenshots
    • Clipboard
    • Open windows
    • The accessibility tree

    It runs as a process separate from aiod, on any host with a desktop. Examples:

    • An Ubuntu desktop
    • A VM with Xvfb or Xvnc
    • A Windows host with an interactive logon session

    aiod proxies its API. It does not touch the desktop itself. That process boundary keeps desktop control optional: aiod runs the same without a desktop present, and reaches one wherever computer-use is running.

    The desktop plane is v2-only, under /v2/computer/*.

    CapabilityRouteUse
    ProbeGET /v2/computer/infodisplay, resolution, what it can do
    ScreenshotGET /v2/computer/screenshotthe current desktop as a PNG
    ActionPOST /v2/computer/actionsmouse, keyboard, scroll, clipboard, window, node; one action per request; ?include_screenshot=true returns a screenshot taken after it
    BatchPOST /v2/computer/actions/batcha sequence of actions in order
    Cursor, clipboard, windowsGET /v2/computer/cursor, /clipboard, /windowsread the current state
    Accessibility treeGET /v2/computer/accessibility, /accessibility/nodesfind elements by role and name
    RecordingPOST /v2/computer/recordstart, status, stop

    v1 keeps two aliases for 1.x clients:

    • POST /v1/browser/actions
    • POST /v1/display/record

    The routes side by side are in Migration from 1.x.

    The first part of this page is the action reference an agent's tools are built on. The second covers the viewers a person uses to watch or take over the same desktop. Browser API covers CDP-based page automation instead, and Computer Use examples walks one desktop task end to end.

    For agent tools

    How the two processes connect

    The worker binds AIO_COMPUTER_USE_LISTEN (default 0.0.0.0:18100). aiod reaches it at AIO_COMPUTER_USE_URL (default http://127.0.0.1:18100) and proxies the desktop routes opaquely. When the worker is not running, the computer routes answer 503:

    {
      "success": false,
      "message": "computer-use worker is not available",
      "data": null,
      "hint": "start computer-use or set AIO_COMPUTER_USE_URL"
    }

    Prerequisites

    Linux: a reachable X11 display. Set DISPLAY, and XAUTHORITY if the display needs cookie authentication. GET /v2/computer/info reports what it found in display and xauthority.

    Windows: an interactive logon session. aiod can run as a Session 0 SCM service; computer-use must run in the interactive session to see the desktop and inject input.

    Start the worker

    computer-use ships with every aiod release; see Releases. On Linux the install script installs both:

    curl -fsSL https://aio-static.tos-cn-beijing.volces.com/install.sh | AIOD_WITH_COMPUTER_USE=1 sh

    On Windows download https://aio-static.tos-cn-beijing.volces.com/latest/windows-x86_64/computer-use.exe.

    It takes no flags and reads only environment variables: DISPLAY and XAUTHORITY pick the display, AIO_COMPUTER_USE_LISTEN sets the listen address.

    Linux:

    DISPLAY=:99 XAUTHORITY=$HOME/.Xauthority computer-use
    curl -s http://127.0.0.1:8091/v2/computer/info

    supervisord, as the Computer image runs it:

    [program:computer-use]
    command=/usr/local/bin/computer-use
    environment=DISPLAY=":99",XAUTHORITY="/home/sandbox/.Xauthority",HOME="/home/sandbox",USER="sandbox"
    user=sandbox
    autorestart=true
    stdout_logfile=/var/log/computer-use.log
    redirect_stderr=true

    Windows: computer-use.exe cannot run as a service; register it as a logon-triggered scheduled task, see Windows.

    Check available capabilities

    GET /v2/computer/info reports the display the worker found and what it can do on it:

    curl "$BASE_URL/v2/computer/info"

    data is:

    • available — whether the worker reached a display
    • display — the display the worker found
    • xauthority — the XAUTHORITY it found
    • screen_resolution — {width, height}
    • capabilities — {screenshot, actions, clipboard, recording}
    • warnings — problems the probe found

    v1 has no desktop info route. The sandbox capability probe reports the plane instead:

    curl "$BASE_URL/v1/capabilities"

    data.computer is:

    • status — ready, degraded, or absent
    • display, resolution — what the worker found
    • screenshot, actions, clipboard, recording, accessibility — one flag per operation
    • accessibility_backend — atspi, uia, or null where none is compiled in
    • provider, missing, warnings — how it was found, and what is not there

    When no worker is running, the response is:

    {
      "status": "absent",
      "provider": null,
      "display": null,
      "screenshot": false,
      "actions": false,
      "clipboard": false,
      "recording": false,
      "accessibility": false,
      "accessibility_backend": null,
      "resolution": {
        "width": null,
        "height": null
      },
      "missing": [
        "computer-use"
      ],
      "warnings": [
        "computer-use worker probe failed: error sending request for url (http://127.0.0.1:18100/capabilities)"
      ]
    }

    When you need the availability and the XAUTHORITY the worker itself reports, switch to v2:

    Take a screenshot

    The response body is the image itself:

    curl "$BASE_URL/v2/computer/screenshot" -o desktop.png

    The current desktop as a PNG, including all windows, the taskbar, and dialogs. The response headers carry the geometry — x-image-width, x-image-height, x-screen-width, x-screen-height.

    v1 has no desktop screenshot route. The post-action frame is the way in, and WAIT is the action that changes nothing:

    curl -X POST "$BASE_URL/v1/browser/actions?include_screenshot=true" \
      -H "Content-Type: application/json" \
      -d '{"action_type": "WAIT", "duration": 0.5}'

    screenshot in the response is the same desktop capture, base64 PNG. The Python SDK sends no query string on this route, so a frame taken this way needs the HTTP call. GET /v1/browser/screenshot is a different thing: it captures the browser page over CDP, not the desktop.

    When you want the capture as a PNG body, with no action attached, switch to v2:

    Act

    One action per request, in screen coordinates. With ?include_screenshot=true the response also carries a screenshot taken after the action:

    curl -X POST "$BASE_URL/v2/computer/actions" \
      -H "Content-Type: application/json" \
      -d '{"action_type": "CLICK", "x": 640, "y": 400}'
    
    curl -X POST "$BASE_URL/v2/computer/actions?include_screenshot=true" \
      -H "Content-Type: application/json" \
      -d '{"action_type": "SCROLL", "dx": 0, "dy": -3}'
    curl -X POST "$BASE_URL/v1/browser/actions" \
      -H "Content-Type: application/json" \
      -d '{"action_type": "CLICK", "x": 640, "y": 400}'
    
    curl -X POST "$BASE_URL/v1/browser/actions?include_screenshot=true" \
      -H "Content-Type: application/json" \
      -d '{"action_type": "SCROLL", "dx": 0, "dy": -3}'

    The alias and the v2 route are one handler: same body, same response.

    The Python SDK sends one typed action per call:

    Python
    TypeScript
    from agent_sandbox import Sandbox
    from agent_sandbox.browser.types.action import Action_Click
    
    client = Sandbox(base_url="http://127.0.0.1:18091")
    client.browser.execute_action(request=Action_Click(x=640, y=400))

    Its action union covers the mouse, the keyboard, and WAIT. The clipboard, window, and node actions below go over HTTP on the same route, as does a post-action screenshot.

    The action body is the same in v1 and v2. For example, click screen coordinates (640, 400):

    {
      "action_type": "CLICK",
      "x": 640,
      "y": 400
    }

    The body describes the action directly, and action_type selects which action to run:

    action_typeRequiredOptional
    MOVE_TOx, y—
    MOVE_RELx_offset, y_offset—
    CLICK—x, y, button, num_clicks
    RIGHT_CLICK—x, y
    DOUBLE_CLICK—x, y
    MOUSE_DOWN—button
    MOUSE_UP—button
    DRAG_TOx, y—
    DRAG_RELx_offset, y_offset—
    SCROLL—dx, dy
    TYPINGtextuse_clipboard
    PRESSkey—
    KEY_DOWNkey—
    KEY_UPkey—
    HOTKEYkeys—
    WAITduration—
    SET_CLIPBOARDtext—
    WINDOW_ACTIVATEwindow_id—
    WINDOW_MINIMIZEwindow_id—
    NODE_FOCUSnode_id—
    NODE_INVOKEnode_idaction
    NODE_SET_VALUEnode_id, value—
    • duration is in seconds; keys is a list, ["ctrl", "c"] for copy.
    • A window_id comes from the window list, a node_id from the accessibility tree; both are below.
    • Two bounds hold on every action, single or batched: a WAIT asks for at most 10 s, and a SCROLL for at most 100 notches per axis.
    • TYPING types ASCII text as keystrokes. Text with other characters goes through the clipboard (use_clipboard, default true), which replaces whatever the clipboard held; with use_clipboard: false such text is refused with 400, since keystrokes would drop the characters. Clipboard text is pasted with Shift+Insert, which terminals and GTK or Chromium windows both accept.
    • A successful PRESS, HOTKEY, or TYPING means the input reached the focused window, not that the application acted on it. Take a screenshot to confirm.
    • The full per-type schema is in the API Reference.

    The response carries status and action_performed, plus the post-action frame as base64 PNG when one was asked for. If capturing it fails, that shows up as screenshot_error next to the action result, not as a failed request: the action already happened.

    A Windows secure desktop (a UAC prompt, the lock screen) refuses input; see Windows.

    Batch several actions

    A batch runs its actions in order in one request, and nothing else touches the display until it finishes:

    curl -X POST "$BASE_URL/v2/computer/actions/batch" \
      -H "Content-Type: application/json" \
      -d '{"include_screenshot": true,
           "actions": [
             {"action_type": "HOTKEY", "keys": ["ctrl", "l"]},
             {"action_type": "TYPING", "text": "example.com"},
             {"action_type": "PRESS", "key": "enter"}
           ]}'

    include_screenshot is a body field here, not a query parameter. At most 50 actions per batch, and their WAIT actions total at most 20 s.

    data is:

    • performed — the actions that ran, in order
    • failed_index, error — which action stopped the batch, and why; everything before it ran
    • status, reason — denied and its machine-readable cause, when the host refused the input
    • screenshot, screenshot_error — the frame after the last action that ran

    When a sequence has to run without anything else touching the display, switch to v2:

    Run your own automation

    The desktop is an ordinary X display and every command runs with DISPLAY set, so a script that drives the screen itself runs through POST /v2/commands (/v1/bash/exec on v1) against the same desktop the actions above use. The Computer image ships xdotool; pyautogui is one pip install pyautogui away:

    sb = Aio(BASE_URL)  # the helper on the examples index
    sb.post("/v2/commands", command="pip install --quiet pyautogui", timeout=280)
    sb.post("/v2/commands", command='''python3 - <<'EOF'
    import pyautogui
    pyautogui.moveTo(320, 240)
    pyautogui.click()
    pyautogui.write("hello")
    pyautogui.screenshot("/tmp/desk.png")
    EOF''')
    print(sb.get("/v2/computer/cursor"))   # {'x': 320, 'y': 240}

    Read the pointer, clipboard, and windows

    EndpointReturnsNotes
    GET /v2/computer/cursorThe current pointer positionScreen coordinates
    GET /v2/computer/clipboardThe clipboard text5 s read timeout, 1 MiB cap
    GET /v2/computer/windowsThe top-level windowsUp to 200 on the xdotool code path

    The window list:

    {snapshot_id, windows: [{window_id, title, process_id, bounds, minimized}]}

    A window_id is the native window handle, not an index into the listing. It stays valid while the window exists; acting on a closed window fails instead of hitting whatever took its place.

    When you need the window list, the pointer position, or the clipboard text, switch to v2:

    Act through the accessibility tree

    Two routes read the desktop's accessibility tree:

    • GET /v2/computer/accessibility returns the full tree.
    • GET /v2/computer/accessibility/nodes searches that tree as a flat list.

    Both accept queries such as ?role=button&name=Sign+in and take the same fields:

    FieldValuesMeaning
    scopeforeground (default), desktopThe active window, or every top-level window
    role, namestringKeep only what matches
    matchsubstring (default), exact, regexHow role and name are compared
    statescomma-separatedStates a node must all carry (enabled,showing)
    include_offscreenboolean, off by defaultInclude nodes the backend marks as offscreen
    max_depth1–64, default 32Maximum depth
    max_nodes1–20000, default 5000Maximum nodes
    timeout_ms100–60000, default 5000Walk timeout
    limit1–1000, default 50nodes only: most nodes returned

    Out-of-range values are clamped, not rejected, and role and name are compared case-insensitively in every mode. A truncated flag marks an answer that hit one of those budgets, so a missing node is not evidence the node is absent. On nodes, ?node_id= resolves one handle and ignores the search parameters.

    A returned node_id stays valid as long as the element exists, across snapshots. It turns 404 once the element is gone — its window closed, or its application restarted.

    Each node carries node_id, role, and name, plus bounds, states, value, and actions, the action names the element advertises:

    {
      "node_id": "a::1.30:/org/a11y/atspi/accessible/247",
      "role": "link",
      "name": "Open the report"
    }

    Put that node_id in an action body to act on the element instead of pixel coordinates:

    {
      "action_type": "NODE_INVOKE",
      "node_id": "a::1.30:/org/a11y/atspi/accessible/247"
    }

    Three node actions:

    • NODE_INVOKE without action runs the node's first advertised action; with one it runs that action, and a 409 lists what the element advertises instead.
    • NODE_FOCUS only moves focus, so a TYPING action can follow.
    • NODE_SET_VALUE takes value and writes it into the element's text or value; an element with neither answers 409.

    When you want to act on an element by role and name instead of pixel coordinates, switch to v2:

    Backends by platform: Windows uses UI Automation (UIA), built into the OS with nothing to install; Linux uses AT-SPI2 (docs).

    Three things make a Linux desktop readable as a tree:

    1. at-spi2-core installed: it provides the org.a11y.Bus D-Bus service and the bus launcher.
    2. A session D-Bus the worker finds through DBUS_SESSION_BUS_ADDRESS.
    3. Applications that expose their own tree. GTK and Qt apps do by default; Chromium and Electron need --force-renderer-accessibility.

    The Computer image ships at-spi2-core, starts a session D-Bus, and runs Chromium with that flag.

    Errors:

    StatusMeaning
    501No backend to run here: a platform with no implementation, or the first two of those missing. Retrying will not change that
    503A backend is there but could not read right now: no interactive desktop, or no focused window

    Record the desktop

    POST /v2/computer/record records with ffmpeg: action is start, status, or stop.

    POST /v1/display/record is the same endpoint: action is start, status, or stop. The Python SDK covers the route and every parameter of it:

    Python
    TypeScript
    client.display.record(action="start", save_path="/workspace/recordings/session.mp4")
    client.display.record(action="stop")

    Recording uses ffmpeg, and only one recording runs at a time. start takes the capture settings:

    ParameterRangeDefault
    fps1–6030
    crf0–51 (lower is higher quality)30
    max_durationup to 600 s60
    width, heightpixelsThe detected screen resolution

    save_path is optional; without one the file lands in the temp directory as recording_<timestamp>.mp4. A missing parent directory is created, and one the worker cannot write to is a 400.

    data carries:

    • status — recording, stopped, or idle
    • duration — seconds recorded so far
    • save_path — where the file is being written
    • file_size_bytes — the size after a stop

    Store recordings under a path the File API can reach. Stop recording before tearing down the host, so the file stays playable.

    Talking to the worker directly

    computer-use also answers on its own port, without going through aiod:

    RoutePurposeNotes
    GET /healthzLivenessPublic
    GET /capabilitiesDesktop probeschema_version 1; needs the API key if set
    GET /openapi.jsonThe worker's own contractPublic
    GET /metricsPrometheus textNeeds the API key if set

    Useful when deploying or health-checking the worker on its own.

    Human in the loop

    A person steps into the same terminal, browser or desktop the agent is driving: to watch a run, to take over a login or a captcha, to hand the desktop back. Nothing in the daemon changes for that. The viewers are pages the prebuilt images serve through their gateway, and each one attaches to the object the agent already uses:

    ViewerPath on the imageShows
    noVNC/vnc/index.htmlThe X display the worker drives; watch or take over
    DevTools/browser-ui, /cdp/devtools/*Chromium's tabs, over the same CDP the agent uses
    WebShell/terminal?session_id=…One PTY session; GET /v1/shell/terminal-url builds the link
    code-server/code-server/An IDE on the same filesystem; off unless DISABLE_CODE_SERVER=false
    JupyterLab/jupyterNotebooks on the same filesystem; off unless DISABLE_JUPYTER=false

    Xvnc is both the X server and the VNC server, so a person's clicks land on the display directly and the worker sees the result in its next screenshot. The daemon serves the API only; aiod alone answers 404 on every path above.

    The prebuilt images wire this up. The AIO image runs Xvnc on DISPLAY=:99.0 with openbox and Chromium, and serves noVNC, DevTools and the WebShell. The Computer image sets AIO_DESKTOP=xfce on top of it: an XFCE session takes the display, the computer-use worker starts next to aiod, a session D-Bus serves AT-SPI, and Chromium runs with --force-renderer-accessibility so tab contents appear in the accessibility tree.