CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
bootstrap.ts353 linesDownload Raw Back to computer-use
1/**2 * @license3 * Copyright 2025 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6 7/**8 * Computer Use bootstrap state machine (cua-driver backend).9 *10 * cua-driver is a persistent daemon (`CuaDriver serve` under com.trycua.driver)11 * fronted by a thin `cua-driver mcp` stdio proxy. Tools only work once the12 * daemon has BOTH macOS grants (Accessibility + Screen Recording).13 *14 * First-use permission flow — driven so the user grants ONE permission at a15 * time, and so we can reliably detect progress (the two problems with the16 * native `permissions grant`: it requests both at once, and while its daemon17 * sits in the all-or-nothing gate `permissions status` reports `unknown`, so a18 * partial grant is undetectable). Instead:19 *20 *   1. Run a status-only daemon with `serve --no-permissions-gate` (launched21 *      via `open -a CuaDriver` so it carries the com.trycua.driver identity).22 *      With the gate off it SERVES IMMEDIATELY even with no grants, so23 *      `permissions status --json` returns accurate PER-PERMISSION booleans.24 *   2. POLL status every 5s. Open the System Settings pane for whichever25 *      permission is still missing — Accessibility first, then Screen26 *      Recording — one at a time, guiding the user.27 *   3. Granting Screen Recording force-restarts the daemon → the next poll28 *      reads `unknown`; we relaunch the status daemon and keep polling.29 *   4. Once both are granted, tear the status daemon down and spawn the real30 *      proxy. Any residual restart is absorbed by the client's reconnect.31 */32 33import { execFile, spawnSync } from 'node:child_process';34import { promisify } from 'node:util';35import { rmSync } from 'node:fs';36import { homedir } from 'node:os';37import { join } from 'node:path';38import type { ComputerUseClient } from './client.js';39import { isPackageSpecApproved, saveInstallState } from './install-state.js';40import { approvalKey, binaryPath } from './constants.js';41import { ensureInstalled } from './downloader.js';42 43const execFileAsync = promisify(execFile);44 45export interface BootstrapContext {46  signal: AbortSignal;47  updateOutput?: (output: string) => void;48  /** Treat the first-use install as pre-approved (YOLO / AUTO_EDIT / AUTO). */49  autoApproveInstall?: boolean;50}51 52/**53 * Result of a permission probe:54 *  - 'ok'              both grants present55 *  - 'accessibility'   Accessibility missing56 *  - 'screenRecording' Accessibility present, Screen Recording missing57 *  - 'unknown'         couldn't read status (no daemon yet / restarting)58 */59export type PermissionProbeResult =60  | 'ok'61  | 'accessibility'62  | 'screenRecording'63  | 'unknown';64 65/** A running status daemon we can tear down. */66export interface StatusDaemon {67  kill: () => void;68}69 70export interface BootstrapDeps {71  homeDir: string;72  approvalKey: string;73  platform: NodeJS.Platform;74  promptInstallApproval: (key: string) => Promise<boolean>;75  install: (onProgress?: (m: string) => void) => Promise<string>;76  /**77   * Launch a status-only daemon (`serve --no-permissions-gate` via78   * `open -a CuaDriver`) so `permissions status` returns per-permission79   * booleans even before any grant. Returns a handle to tear it down.80   */81  startStatusDaemon: () => StatusDaemon;82  /** Read current TCC status (`permissions status --json`). */83  probePermissions: () => Promise<PermissionProbeResult>;84  /** Open the System Settings pane for one permission so the user can grant it. */85  openPermissionPane: (kind: 'accessibility' | 'screenRecording') => void;86  /** Poll interval. Default 5000ms. */87  pollIntervalMs?: number;88  /** Total poll timeout. Default 10 min. */89  pollTimeoutMs?: number;90}91 92/**93 * Parse `cua-driver permissions status --json` into a probe result.94 * Shape: `{ accessibility: bool, screen_recording: bool, ... }`.95 */96export function parsePermissionsStatus(json: string): PermissionProbeResult {97  try {98    const o = JSON.parse(json) as {99      accessibility?: boolean;100      screen_recording?: boolean;101    };102    if (typeof o.accessibility !== 'boolean') return 'unknown';103    if (!o.accessibility) return 'accessibility';104    if (!o.screen_recording) return 'screenRecording';105    return 'ok';106  } catch {107    return 'unknown';108  }109}110 111const SOCKET = () =>112  join(homedir(), 'Library', 'Caches', 'cua-driver', 'cua-driver.sock');113 114function killServeDaemons(): void {115  try {116    spawnSync(117      'pkill',118      ['-f', 'CuaDriver.app/Contents/MacOS/cua-driver serve'],119      {120        stdio: 'ignore',121      },122    );123  } catch {124    // ignore125  }126  try {127    rmSync(SOCKET(), { force: true });128  } catch {129    // ignore130  }131}132 133/** Probe via the window-free `permissions status --json` CLI (non-blocking). */134export async function probePermissionsViaStatus(): Promise<PermissionProbeResult> {135  try {136    const { stdout } = await execFileAsync(137      binaryPath(homedir()),138      ['permissions', 'status', '--json'],139      { timeout: 10_000, env: process.env as NodeJS.ProcessEnv },140    );141    return parsePermissionsStatus(stdout);142  } catch {143    return 'unknown';144  }145}146 147/**148 * Launch the status-only daemon. `open -a CuaDriver` gives it the149 * com.trycua.driver TCC identity; `--no-permissions-gate` makes it serve150 * immediately so status reads work before grants land. Kills any prior daemon151 * first so there is exactly one.152 */153export function startStatusDaemonProcess(): StatusDaemon {154  killServeDaemons();155  try {156    spawnSync(157      'open',158      [159        '-n',160        '-g',161        '-a',162        'CuaDriver',163        '--args',164        'serve',165        '--no-permissions-gate',166      ],167      { stdio: 'ignore' },168    );169  } catch {170    // ignore — the poll loop reports 'unknown' and retries.171  }172  return { kill: killServeDaemons };173}174 175/** Open the System Settings privacy pane for a permission. */176export function openPermissionPaneProcess(177  kind: 'accessibility' | 'screenRecording',178): void {179  const anchor =180    kind === 'accessibility'181      ? 'Privacy_Accessibility'182      : 'Privacy_ScreenCapture';183  try {184    spawnSync(185      'open',186      [`x-apple.systempreferences:com.apple.preference.security?${anchor}`],187      { stdio: 'ignore' },188    );189  } catch {190    // ignore — the message still tells the user where to go.191  }192}193 194/** Production defaults — instantiated lazily so tests can override per call. */195function defaultDeps(): BootstrapDeps {196  const home = homedir();197  return {198    homeDir: home,199    approvalKey: approvalKey(),200    platform: process.platform,201    promptInstallApproval: async (key) => {202      process.stderr.write(203        `\n[Computer Use] First-time setup\n` +204          `  Driver: ${key}\n` +205          `  This downloads a ~20MB signed + notarized binary into ~/.qwen/computer-use/.\n` +206          `  Computer Use can click, type, and read your desktop apps in the background.\n` +207          `  On macOS you'll be guided through Accessibility and Screen Recording permissions next.\n` +208          `Set QWEN_COMPUTER_USE_AUTO_APPROVE=1 to skip this prompt.\n`,209      );210      return process.env['QWEN_COMPUTER_USE_AUTO_APPROVE'] === '1';211    },212    install: (onProgress) => ensureInstalled({ home, onProgress }),213    startStatusDaemon: startStatusDaemonProcess,214    probePermissions: probePermissionsViaStatus,215    openPermissionPane: openPermissionPaneProcess,216  };217}218 219export async function runBootstrap(220  client: ComputerUseClient,221  ctx: BootstrapContext,222  depsOverride?: Partial<BootstrapDeps>,223): Promise<void> {224  const deps: BootstrapDeps = { ...defaultDeps(), ...depsOverride };225  const pollIntervalMs = deps.pollIntervalMs ?? 5000;226  const pollTimeoutMs = deps.pollTimeoutMs ?? 10 * 60_000;227 228  // A warm client (already started this session) has already passed the install229  // gate, the download, and the permission flow — short-circuit before any of230  // them run again. This MUST precede the install gate and `deps.install()`:231  // a started client implies the binary already exists, and otherwise a unit232  // test that injects a started fake client still triggers the real downloader233  // (network + ~20MB) and writes install-state into the repo CWD. (review #1)234  if (client.isStarted()) return;235 236  // Step 1: install approval gate (gates the download).237  const approved = await isPackageSpecApproved(deps.homeDir, deps.approvalKey);238  if (!approved) {239    if (ctx.autoApproveInstall) {240      ctx.updateOutput?.('Computer Use install auto-approved (approval mode).');241    } else {242      ctx.updateOutput?.(243        'Computer Use needs a one-time driver download (first use).',244      );245      const ok = await deps.promptInstallApproval(deps.approvalKey);246      if (!ok) {247        throw new Error(248          `Computer Use install declined by user. Re-invoke the tool to be prompted again.`,249        );250      }251    }252    await saveInstallState(deps.homeDir, {253      approvedPackageSpec: deps.approvalKey,254      approvedAtIso: new Date().toISOString(),255    });256  }257 258  // Step 2: ensure the binary is present (download on first use; no-op after).259  await deps.install(ctx.updateOutput);260 261  // Step 3: macOS permission flow (one permission at a time; see file header).262  if (deps.platform === 'darwin') {263    await ensurePermissions(deps, ctx, pollIntervalMs, pollTimeoutMs);264  }265 266  // Step 4: spawn the proxy against the now-granted daemon.267  await client.start(ctx.updateOutput);268}269 270async function ensurePermissions(271  deps: BootstrapDeps,272  ctx: BootstrapContext,273  pollIntervalMs: number,274  pollTimeoutMs: number,275): Promise<void> {276  // A status-only (no-gate) daemon so `permissions status` reports per-277  // permission booleans throughout — this is what makes partial grants278  // detectable and lets us guide one permission at a time.279  let daemon = deps.startStatusDaemon();280  let openedAccessibility = false;281  let openedScreenRecording = false;282 283  try {284    const startedAt = Date.now();285    for (;;) {286      if (ctx.signal.aborted)287        throw new Error('Computer Use bootstrap aborted.');288      if (Date.now() - startedAt > pollTimeoutMs) {289        throw new Error(290          `Computer Use permission grant timed out after ${Math.round(291            pollTimeoutMs / 1000,292          )}s. Re-invoke the tool to retry.`,293        );294      }295      await sleep(pollIntervalMs);296      const probe = await deps.probePermissions();297 298      if (probe === 'ok') return;299 300      if (probe === 'unknown') {301        // No serving daemon — first launch still coming up, or the daemon was302        // restarted by a Screen-Recording grant. Relaunch and keep polling.303        daemon.kill();304        daemon = deps.startStatusDaemon();305        const elapsed = Math.round((Date.now() - startedAt) / 1000);306        ctx.updateOutput?.(307          `Bringing up Computer Use permissions check… (${elapsed}s)`,308        );309        continue;310      }311 312      if (probe === 'accessibility') {313        if (!openedAccessibility) {314          openedAccessibility = true;315          deps.openPermissionPane('accessibility');316          ctx.updateOutput?.(317            'Step 1/2 — In the System Settings window that opened ' +318              '(Privacy & Security → Accessibility), turn ON CuaDriver. ' +319              'This continues automatically.',320          );321        } else {322          const elapsed = Math.round((Date.now() - startedAt) / 1000);323          ctx.updateOutput?.(324            `Waiting for Accessibility… (${elapsed}s) — enable CuaDriver in System Settings.`,325          );326        }327        continue;328      }329 330      // probe === 'screenRecording'331      if (!openedScreenRecording) {332        openedScreenRecording = true;333        deps.openPermissionPane('screenRecording');334        ctx.updateOutput?.(335          'Step 2/2 — Accessibility granted. Now in System Settings ' +336            '(Privacy & Security → Screen & System Audio Recording), turn ON ' +337            'CuaDriver. macOS will ask to restart CuaDriver — allow it; that is ' +338            'expected. This continues automatically.',339        );340      } else {341        const elapsed = Math.round((Date.now() - startedAt) / 1000);342        ctx.updateOutput?.(343          `Waiting for Screen Recording… (${elapsed}s) — enable CuaDriver in System Settings.`,344        );345      }346    }347  } finally {348    daemon.kill();349  }350}351 352const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));353 
basant307/AI_Governance_Project · CoolFace