{"record":{"id":"c4e8d73bca5f83a1","repo":"Hmbown/CodeWhale","slug":"consent-required","errorCode":"consent_required","errorMessage":"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}\"}.","messagePattern":"Codewhale needs the user's permission to use (.+?) on this computer — ask them, then record their answer with consent (.+?)\\.","errorType":"error_code","errorClass":"ServerError","httpStatus":null,"severity":"error","filePath":"crates/tui/plugins/computer-use/mcp/server.mjs","lineNumber":584,"sourceCode":"    const bound = boundApps.get(computer.id);\n    if (!refs.length && bound && BOUND_TARGET_TOOLS.has(name)) refs.push(bound);\n  }\n  let grant = null;\n  for (const ref of refs) {\n    // A state or element whose backend reported no identity at all has no app\n    // to consent to — observation never named one either, so there is nothing\n    // a recorded decision could match.\n    if (!ref || !consent.appKeys(ref).length) continue;\n    const { verdict, ref: known } = await consentForRef(computer, ref);\n    const desc = known.name ?? known.bundle_id ?? (known.pid ? `pid ${known.pid}` : \"the application\");\n    const arg = known.bundle_id ?? known.name ?? (known.pid ? `pid:${known.pid}` : \"the app\");\n    if (verdict.state === \"denied\") {\n      throw new ServerError(\"app_denied\",\n        `the user denied access to ${desc} on this computer — do not work around it; only they can change it (consent {action:\"revoke\"}).`,\n        { app: known });\n    }\n    if (verdict.state === \"undecided\") {\n      throw new ServerError(\"consent_required\",\n        `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}\"}.`,\n        { app: known });\n    }\n    grant = { ref: known, persisted: verdict.persisted === true };\n  }\n  // Taking the shared pointer/focus is a second, separate consent: the first\n  // activate:true is the moment the agent stops being background — on every\n  // platform, not just macOS.\n  if (name === \"open_application\" && args.activate === true) {\n    const fg = consent.foregroundDecision(computer.id);\n    if (fg.state === \"denied\") {\n      throw new ServerError(\"foreground_denied\",\n        `the user denied shared-desktop (foreground) control on this computer — continue with open_application activate:false (background control) or ask them to reconsider.`,\n        { scope: \"foreground\" });\n    }\n    if (fg.state === \"undecided\") {\n      throw new ServerError(\"foreground_consent_required\",\n        `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.`,","sourceCodeStart":566,"sourceCodeEnd":602,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/plugins/computer-use/mcp/server.mjs#L566-L602","documentation":"Thrown when a tool call targets an application whose consent verdict is \"undecided\" — the user has never allowed or denied it. The computer-use server requires an explicit recorded user decision before an agent may act on a local application, and this error tells the agent exactly how to record one: ask the user, then call consent with {action:\"allow\"|\"deny\", app:\"<arg>\"}.","triggerScenarios":"First tool call that resolves an app identity via consentForRef() where no consent record exists (consent.appKeys(ref) was empty, so no recorded decision could match) and verdict.state === \"undecided\".","commonSituations":"First-ever automation of a new app on the machine; a fresh session with only session-scoped decisions; a container/registry reset clearing persisted consent records.","solutions":["Ask the user for permission to use the named application.","Record the answer with consent {action:\"allow\" or \"deny\", app:\"<bundle_id or name>\"}.","Retry the original tool call — it will now pass the consent gate."],"exampleFix":"// before: calling the tool with no recorded decision\nawait callTool({ name: \"screenshot\", arguments: { app: \"com.apple.Safari\" } }); // throws consent_required\n\n// after: record the user's answer first, then call\nawait callTool({ name: \"consent\", arguments: { action: \"allow\", app: \"com.apple.Safari\" } });\nawait callTool({ name: \"screenshot\", arguments: { app: \"com.apple.Safari\" } });","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  return await callTool({ name, arguments });\n} catch (e) {\n  if (e.code === \"consent_required\") {\n    const arg = e.data?.app?.bundle_id ?? e.data?.app?.name;\n    const answer = await askUser(`May I use ${arg}?`);\n    await callTool({ name: \"consent\", arguments: { action: answer ? \"allow\" : \"deny\", app: arg } });\n    return await callTool({ name, arguments }); // single retry\n  }\n  throw e;\n}","preventionTips":["Record consent for every app before the first tool call against it.","Reuse the app identifier from the error's data.app field verbatim in the consent call.","Batch consent setup at the start of a session for all apps the workflow will touch."],"tags":["consent","user-permission","computer-use","mcp"],"backgroundTag":"authentication-required","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T16:17:23.217Z"}