Hmbown/CodeWhale · error · ServerError

foreground_consent_required

foreground_consent_required

Error message

open_application activate:true would take this computer's shared pointer and focus — ask the user, then record their answer with consent {action:"allow"|"deny", scope:"foreground"}. Background control (activate:false) needs no such consent.

What it means

Thrown when open_application is called with activate:true and the foreground-consent decision is "undecided" — the user has never allowed or denied shared-desktop control. Because activate:true would take the computer's shared pointer and focus, the server requires an explicit recorded decision first and explains the consent call syntax; background control (activate:false) is explicitly exempt.

Solutions

  1. Ask the user whether the agent may take the shared pointer/focus.
  2. Record the answer: consent {action:"allow", scope:"foreground"} or {action:"deny", scope:"foreground"}.
  3. Alternatively use activate:false, which needs no foreground consent.
  4. Retry open_application activate:true after the decision is recorded.

Example fix

// before: activate:true with no foreground decision
await callTool({ name: "open_application", arguments: { name: "Firefox", activate: true } }); // throws foreground_consent_required

// after: ask, record, then activate
await callTool({ name: "consent", arguments: { action: "allow", scope: "foreground" } });
await callTool({ name: "open_application", arguments: { name: "Firefox", activate: true } });
Defensive patterns

Strategy: try-catch

Try / catch

try {
  return await callTool({ name: "open_application", arguments: { ...args, activate: true } });
} catch (e) {
  if (e.code === "foreground_consent_required") {
    const ok = await askUser("May I take the mouse/keyboard focus?");
    await callTool({ name: "consent", arguments: { action: ok ? "allow" : "deny", scope: "foreground" } });
    if (!ok) return callTool({ name: "open_application", arguments: { ...args, activate: false } });
    return await callTool({ name: "open_application", arguments: { ...args, activate: true } });
  }
  throw e;
}

Prevention

When it happens

Trigger: open_application with args.activate === true while consent.foregroundDecision(computer.id).state === "undecided" — i.e. no allow/deny recorded for scope:"foreground" on this computer.

Common situations: First foreground launch of an app in a new session; a script always requests activate:true without ever recording a foreground decision; persisted foreground consent absent on a new machine.

Related errors


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

Appendix: source

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

    if (verdict.state === "undecided") {
      throw new ServerError("consent_required",
        `Codewhale needs the user's permission to use ${desc} on this computer — ask them, then record their answer with consent {action:"allow"|"deny", app:"${arg}"}.`,
        { app: known });
    }
    grant = { ref: known, persisted: verdict.persisted === true };
  }
  // Taking the shared pointer/focus is a second, separate consent: the first
  // activate:true is the moment the agent stops being background — on every
  // platform, not just macOS.
  if (name === "open_application" && args.activate === true) {
    const fg = consent.foregroundDecision(computer.id);
    if (fg.state === "denied") {
      throw new ServerError("foreground_denied",
        `the user denied shared-desktop (foreground) control on this computer — continue with open_application activate:false (background control) or ask them to reconsider.`,
        { scope: "foreground" });
    }
    if (fg.state === "undecided") {
      throw new ServerError("foreground_consent_required",
        `open_application activate:true would take this computer's shared pointer and focus — ask the user, then record their answer with consent {action:"allow"|"deny", scope:"foreground"}. Background control (activate:false) needs no such consent.`,
        { scope: "foreground" });
    }
  }
  return grant ? { grant } : null;
}

// ---------- tool dispatch ----------
async function callTool(params) {
  const requested = params.name;
  if (!TOOL_NAMES.has(requested)) {
    return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: { code: "unknown_tool", message: `unknown tool "${requested}"` } }) }], isError: true };
  }
  // Merged tools (click, pointer, clipboard, recording, computer, key+duration)
  // resolve to the wire tool they dispatch to before any gate below, so they
  // cannot bypass required args, the kill switch or routing. Wire names stay
  // callable as aliases.
  let name = requested;

View on GitHub (pinned to 73e0f67d83)