Hmbown/CodeWhale · error · ServerError

unsupported_on_transport

unsupported_on_transport

Error message

app_script runs on the local computer only — the ${computer.transport} transport stays a computer-use channel, never a shell

What it means

app_script executes local application scripting and is deliberately restricted to computers with the local transport. On ssh/docker (or any remote transport) the server refuses before dispatching, so a remote computer-use channel can never be used as a shell. Remote agents enforce the same gate, so there is no hop that would honor it.

Solutions

  1. Run app_script against a computer whose transport is "local"
  2. For remote machines use the proper remote tools (screenshot, type, key, etc.) instead of scripting
  3. If you need remote shell access, use a dedicated SSH/shell mechanism, not the computer-use channel
  4. Add/configure a local computer entry if local scripting is genuinely required

Example fix

// before
await appScript({ computer: "ssh-box", script: "ls" })
// after
await appScript({ computer: "local", script: "ls" })
Defensive patterns

Strategy: validation

Validate before calling

if (computer.transport !== "local") throw new Error("app_script is local-only; use remote computer-use tools for this transport");

Type guard

const isLocal = (c) => c?.transport === "local";

Try / catch

try { await appScript({ computer, script }) } catch (e) { if (e.code === "unsupported_on_transport") { /* fall back to remote tools or select a local computer */ } else throw e; }

Prevention

When it happens

Trigger: Calling app_script while the bound computer's transport is not "local" — e.g. an ssh or docker computer where the tool was expected to run commands remotely.

Common situations: Using computer-use against an ssh machine and trying to run scripts; assuming app_script is a general remote-execution tool; environments where only remote computers are configured.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/7b54b2eaabe21f9d. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/plugins/computer-use/mcp/server.mjs:912

      const focused = await callTool({ name: "focus", arguments: { target: args.target, computer: computer.id } });
      const focusBody = JSON.parse(focused.content[0].text);
      // For a chord the element's window is what matters — key equivalents
      // dispatch at window level, so a focus refusal must not block delivery.
      // Text is different: characters go to the first responder, so a field
      // that could not be focused cannot receive the string either.
      if (name === "type" && (focused.isError || focusBody.ok === false)) {
        return { content: [{ type: "text", text: JSON.stringify(fail(computer, focusBody.error?.code ?? "focus_failed", focusBody.error?.message ?? "element could not be focused", { tool: name, stage: "focus" })) }], isError: true };
      }
      args = { ...args };
    }
    // Out-of-process runners (the desktop app for the local computer, the
    // remote agent for ssh computers) get the request over the wire.
    const backendMethod = BACKEND_METHOD[name];
    // Scripting is honored on the local computer only. Remote agents refuse
    // it too (their handler gates computerId), so a remote channel can never
    // be steered into a shell — fail here first to save the hop.
    if (name === "app_script" && computer.transport !== "local") {
      throw new ServerError("unsupported_on_transport", `app_script runs on the local computer only — the ${computer.transport} transport stays a computer-use channel, never a shell`);
    }
    let data;
    const ex = computer.transport === "local" || computer.transport === "ssh" || computer.transport === "docker" ? await executorFor(computer, binding) : null;
    if (ex?.kind === "app") binding.usedApp = true;
    // Zoom needs the bound parent raster up front (server-side check too, not
    // only the backend) so it can bind the child raster after success.
    let zoomParent = null;
    if (name === "zoom") {
      zoomParent = lastRasters.get(computer.id);
      if (!zoomParent) throw new ServerError("no_raster", "no screenshot bound on this computer yet — call screenshot first so zoom has a source raster");
      if (!Array.isArray(args.region) || args.region.length !== 4) throw new ServerError("bad_args", "zoom needs region [x, y, w, h] in last-raster pixels");
    }
    const sink = { reacquired: false };

    if (typeof ex?.remote === "function" && REMOTE_TOOLS.has(backendMethod)) {
      // ssh rides the persistent agent channel when the remote supports
      // --serve; a channel that never produced a reply means an old agent,
      // so fall back to one-shot for that binding rather than failing.

View on GitHub (pinned to 73e0f67d83)