Code

/**
 * mcp/skills/checkAppPermissions.ts — check_app_permissions skill
 *
 * Checks what system permissions an application has been granted (Full Disk
 * Access, Accessibility, Camera, Microphone, etc.). Use before reinstalling
 * software or when an app reports permission errors.
 *
 * Platform strategy
 * -----------------
 * darwin  sqlite3 query against ~/Library/Application Support/com.apple.TCC/TCC.db
 * win32   PowerShell registry query HKCU:\Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager
 *
 * Smoke test
 *   npx tsx -r dotenv/config mcp/skills/checkAppPermissions.ts "CrowdStrike Falcon"
 */
 
import * as os       from "os";
import * as nodePath from "path";
import { exec }      from "child_process";
import { promisify } from "util";
import { z }         from "zod";
 
const execAsync = promisify(exec);
 
// -- Meta ---------------------------------------------------------------------
 
export const meta = {
  name: "check_app_permissions",
  description:
    "Checks what system permissions an application has been granted " +
    "(Full Disk Access, Accessibility, Camera, Microphone, etc.). " +
    "Use before reinstalling software or when an app reports permission errors.",
  riskLevel:       "low",
  destructive:     false,
  requiresConsent: false,
  supportsDryRun:  false,
  affectedScope:   ["user"],
  auditRequired:   false,
  schema: {
    appName: z
      .string()
      .describe("Application name to check (e.g. 'CrowdStrike Falcon', 'Jamf Connect')"),
  },
} as const;
 
// -- Types --------------------------------------------------------------------
 
interface PermissionEntry {
  category: string;
  status:   "allowed" | "denied" | "unknown";
}
 
interface PermissionsResult {
  appName:     string;
  platform:    string;
  permissions: PermissionEntry[];
  error?:      string;
}
 
// -- PowerShell helper --------------------------------------------------------
 
async function runPS(script: string): Promise<string> {
  const encoded = Buffer.from(script, "utf16le").toString("base64");
  const { stdout } = await execAsync(
    `powershell.exe -NoProfile -NonInteractive -EncodedCommand ${encoded}`,
    { maxBuffer: 10 * 1024 * 1024 },
  );
  return stdout.trim();
}
 
// -- darwin implementation ----------------------------------------------------
 
// TCC privacy category service names -> human-readable labels
const TCC_CATEGORIES: Record<string, string> = {
  kTCCServiceSystemPolicyAllFiles:    "Full Disk Access",
  kTCCServiceAccessibility:           "Accessibility",
  kTCCServiceCamera:                  "Camera",
  kTCCServiceMicrophone:              "Microphone",
  kTCCServiceCalendar:                "Calendar",
  kTCCServiceAddressBook:             "Contacts",
  kTCCServicePhotos:                  "Photos",
  kTCCServiceScreenCapture:           "Screen Recording",
  kTCCServiceLocation:                "Location",
  kTCCServiceBluetoothAlways:         "Bluetooth",
  kTCCServiceUserAvailability:        "Focus Status",
  kTCCServiceAppleEvents:             "Automation",
  kTCCServiceSystemPolicySysAdminFiles: "Administrator Files",
  kTCCServiceSpeechRecognition:       "Speech Recognition",
  kTCCServiceMediaLibrary:            "Media Library",
  kTCCServiceReminders:               "Reminders",
  kTCCServiceLiverpool:               "Location Services",
  kTCCServiceShareKit:                "Share Menu",
  kTCCServicePostEvent:               "Input Monitoring",
};
 
async function checkAppPermissionsDarwin(appName: string): Promise<PermissionsResult> {
  // Security: validate appName before using in shell
  if (!/^[a-zA-Z0-9 _\-.']+$/.test(appName)) {
    throw new Error(`[check_app_permissions] Invalid appName: ${appName}`);
  }
 
  const tccDb = nodePath.join(
    os.homedir(),
    "Library",
    "Application Support",
    "com.apple.TCC",
    "TCC.db",
  );
 
  const safeAppName = appName.replace(/'/g, "''");
  // Query TCC.db for entries where client (bundle ID) or policy_id contains the app name
  const query = `SELECT service, client, auth_value FROM access WHERE LOWER(client) LIKE LOWER('%${safeAppName}%') OR LOWER(policy_id) LIKE LOWER('%${safeAppName}%');`;
 
  try {
    const { stdout } = await execAsync(
      `sqlite3 '${tccDb.replace(/'/g, "'\\''")}' "${query}" 2>&1`,
      { maxBuffer: 5 * 1024 * 1024 },
    );
 
    const rows = stdout.trim().split("\n").filter(Boolean);
 
    if (rows.length === 0) {
      return {
        appName,
        platform:    "darwin",
        permissions: [],
        error:       "No TCC entries found for this application. App may not have requested permissions yet, or requires elevated access to read system TCC database.",
      };
    }
 
    const permissions: PermissionEntry[] = rows.map((row) => {
      const parts     = row.split("|");
      const service   = parts[0] ?? "";
      const authValue = parseInt(parts[2] ?? "0", 10);
      // auth_value: 0 = denied, 2 = allowed, 3 = limited/not determined
      const status: PermissionEntry["status"] =
        authValue === 2 ? "allowed" :
        authValue === 0 ? "denied"  :
        "unknown";
      return {
        category: TCC_CATEGORIES[service] ?? service,
        status,
      };
    });
 
    return { appName, platform: "darwin", permissions };
  } catch (err) {
    // System TCC.db may require Full Disk Access — try system-level db
    const systemTccDb = "/Library/Application Support/com.apple.TCC/TCC.db";
    try {
      const { stdout } = await execAsync(
        `sqlite3 '${systemTccDb}' "${query}" 2>&1`,
        { maxBuffer: 5 * 1024 * 1024 },
      );
      const rows = stdout.trim().split("\n").filter(Boolean);
      const permissions: PermissionEntry[] = rows.map((row) => {
        const parts     = row.split("|");
        const service   = parts[0] ?? "";
        const authValue = parseInt(parts[2] ?? "0", 10);
        const status: PermissionEntry["status"] =
          authValue === 2 ? "allowed" :
          authValue === 0 ? "denied"  :
          "unknown";
        return { category: TCC_CATEGORIES[service] ?? service, status };
      });
      return { appName, platform: "darwin", permissions };
    } catch {
      return {
        appName,
        platform:    "darwin",
        permissions: [],
        error:       `Could not read TCC database: ${(err as Error).message}. Full Disk Access may be required.`,
      };
    }
  }
}
 
// -- win32 implementation -----------------------------------------------------
 
const WIN_CAPABILITIES = [
  "webcam",
  "microphone",
  "location",
  "contacts",
  "calendar",
  "phoneCall",
  "email",
  "appointments",
  "userAccountInformation",
  "radios",
  "bluetoothSync",
  "appDiagnostics",
  "gazeInput",
  "broadFileSystemAccess",
];
 
async function checkAppPermissionsWin32(appName: string): Promise<PermissionsResult> {
  if (!/^[a-zA-Z0-9 _\-.']+$/.test(appName)) {
    throw new Error(`[check_app_permissions] Invalid appName: ${appName}`);
  }
 
  const safeApp = appName.replace(/'/g, "''");
  const caps    = WIN_CAPABILITIES.map((c) => `'${c}'`).join(",");
 
  const ps = `
$ErrorActionPreference = 'SilentlyContinue'
$basePath = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\CapabilityAccessManager\\ConsentStore'
$results = @()
foreach ($cap in @(${caps})) {
  $capPath = "$basePath\\$cap"
  if (Test-Path $capPath) {
    $apps = Get-ChildItem -Path $capPath -ErrorAction SilentlyContinue
    foreach ($app in $apps) {
      if ($app.PSChildName -like "*${safeApp}*") {
        $val = (Get-ItemProperty -Path $app.PSPath -Name 'Value' -ErrorAction SilentlyContinue).Value
        $results += [PSCustomObject]@{
          category = $cap
          status   = if ($val -eq 'Allow') { 'allowed' } elseif ($val -eq 'Deny') { 'denied' } else { 'unknown' }
        }
      }
    }
  }
}
$results | ConvertTo-Json -Depth 2 -Compress`.trim();
 
  try {
    const raw = await runPS(ps);
    if (!raw || raw === "null") {
      return { appName, platform: "win32", permissions: [], error: "No capability entries found for this application." };
    }
    const parsed      = JSON.parse(raw) as PermissionEntry | PermissionEntry[];
    const permissions = Array.isArray(parsed) ? parsed : [parsed];
    return { appName, platform: "win32", permissions };
  } catch (err) {
    return {
      appName,
      platform:    "win32",
      permissions: [],
      error:       (err as Error).message,
    };
  }
}
 
// -- Exported run function ----------------------------------------------------
 
export async function run({ appName }: { appName: string }) {
  const platform = os.platform();
  return platform === "win32"
    ? checkAppPermissionsWin32(appName)
    : checkAppPermissionsDarwin(appName);
}
 
// -- CLI smoke test -----------------------------------------------------------
 
if (false) {
  run({ appName: process.argv[2] ?? "Slack" })
    .then((r) => console.log(JSON.stringify(r, null, 2)))
    .catch((err: Error) => { console.error(err.message); process.exit(1); });
}