Code

/**
 * mcp/skills/checkMdmEnrollment.ts — check_mdm_enrollment skill
 *
 * Checks whether this device is enrolled in an MDM (Mobile Device Management)
 * system such as Jamf, Intune, or Apple Business Manager. MDM enrollment
 * enables remote management and policy-pushed reinstalls.
 *
 * Platform strategy
 * -----------------
 * darwin  `profiles status -type enrollment`, `sudo -n profiles show -type enrollment`,
 *         `jamf checkJSSConnection`, `system_profiler SPConfigurationProfileDataType`
 * win32   PowerShell registry check under HKLM:\SOFTWARE\Microsoft\Enrollments
 *
 * Hang prevention
 * ---------------
 * Every `exec` call carries a 5 s internal timeout, and the whole platform call
 * races a 8 s top-level budget so a wedged subprocess can never block past the
 * G3 probe (3 s) or trip the G4 global tool timeout (30 s). `sudo` is invoked
 * with `-n` (non-interactive) so it fails fast when cached credentials are
 * absent, instead of hanging on a TTY-less password prompt.
 *
 * Smoke test
 *   npx tsx -r dotenv/config mcp/skills/checkMdmEnrollment.ts
 */
 
import * as os         from "os";
import { execAsync }   from "./_shared/platform";
 
// -- Internal timing constants ------------------------------------------------
//
// Per-exec timeout (5 s) is well below G3's per-tool budget (3 s × parallel)
// and the G4 global tool timeout (30 s).  The top-level race budget (8 s) is
// the worst-case for a tool that must run several `exec` calls in series on a
// healthy MDM-enrolled Mac.
 
const EXEC_TIMEOUT_MS    = 5_000;
const OVERALL_BUDGET_MS  = 8_000;
const EXEC_KILL_SIGNAL   = "SIGKILL" as const;
 
// -- Meta ---------------------------------------------------------------------
 
export const meta = {
  name: "check_mdm_enrollment",
  description:
    "Checks whether this device is enrolled in an MDM (Mobile Device Management) " +
    "system such as Jamf, Intune, or Apple Business Manager. MDM enrollment enables " +
    "remote management and policy-pushed reinstalls.",
  riskLevel:       "low",
  destructive:     false,
  requiresConsent: false,
  supportsDryRun:  false,
  affectedScope:   ["user"],
  auditRequired:   false,
  schema: {},
} as const;
 
// -- Types --------------------------------------------------------------------
 
type EnrollmentType = "user_approved" | "device_enrollment" | "none" | "unknown";
 
/**
 * `source` records how the result was produced so downstream code (and the
 * LLM scratchpad) can distinguish a confirmed not-enrolled state from a
 * partial probe that timed out before it could rule MDM enrollment in or out.
 */
type ResultSource = "full" | "fast-path" | "timeout" | "error";
 
interface MdmEnrollmentResult {
  isEnrolled:          boolean;
  mdmProvider:         string | null;
  enrollmentType:      EnrollmentType;
  serverUrl:           string | null;
  supervised:          boolean | null;
  lastCheckinAttempt:  string | null;
  source:              ResultSource;
}
 
function emptyResult(source: ResultSource): MdmEnrollmentResult {
  return {
    isEnrolled:         false,
    mdmProvider:        null,
    enrollmentType:     "none",
    serverUrl:          null,
    supervised:         null,
    lastCheckinAttempt: null,
    source,
  };
}
 
// -- 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:  20 * 1024 * 1024,
      timeout:    EXEC_TIMEOUT_MS,
      killSignal: EXEC_KILL_SIGNAL,
    },
  );
  return stdout.trim();
}
 
// -- darwin implementation ----------------------------------------------------
 
async function checkMdmEnrollmentDarwin(): Promise<MdmEnrollmentResult> {
  const result = emptyResult("fast-path");
 
  // 1. profiles status -type enrollment — fast, no sudo, available on all Macs
  try {
    const { stdout } = await execAsync(
      "profiles status -type enrollment 2>/dev/null",
      {
        maxBuffer:  1024 * 1024,
        timeout:    EXEC_TIMEOUT_MS,
        killSignal: EXEC_KILL_SIGNAL,
      },
    );
    const enrollmentLine = stdout.split("\n").find((l) => /MDM enrollment/i.test(l)) ?? "";
    if (/Yes/i.test(enrollmentLine)) {
      result.isEnrolled     = true;
      result.enrollmentType = /User Approved/i.test(enrollmentLine)
        ? "user_approved"
        : "device_enrollment";
    }
  } catch {
    // profiles not available or permission denied — fall through to other probes
  }
 
  // 2. sudo -n profiles show -type enrollment (more details).
  //    `-n` is critical: if cached sudo credentials aren't present, sudo exits
  //    immediately rather than prompting for a password on a non-existent TTY
  //    (which is what was hanging the call for the full 30 s tool timeout).
  try {
    const { stdout } = await execAsync(
      "sudo -n profiles show -type enrollment 2>/dev/null",
      {
        maxBuffer:  2 * 1024 * 1024,
        timeout:    EXEC_TIMEOUT_MS,
        killSignal: EXEC_KILL_SIGNAL,
      },
    );
 
    const urlMatch = stdout.match(/ServerURL\s*=\s*"?([^";\n]+)/i);
    if (urlMatch) result.serverUrl = urlMatch[1].trim();
 
    if (!result.mdmProvider && result.serverUrl) {
      if (/jamf/i.test(result.serverUrl))            result.mdmProvider = "Jamf";
      if (/intune|microsoft/i.test(result.serverUrl)) result.mdmProvider = "Microsoft Intune";
      if (/apple/i.test(result.serverUrl))           result.mdmProvider = "Apple MDM";
      if (!result.mdmProvider)                       result.mdmProvider = result.serverUrl;
    }
 
    const checkinMatch = stdout.match(/LastCheckin\s*=\s*([^\n;]+)/i);
    if (checkinMatch) result.lastCheckinAttempt = checkinMatch[1].trim();
 
    // Reaching this branch means sudo ran — promote the source label.
    result.source = "full";
  } catch {
    // sudo -n failed (no cached creds) OR profiles failed — keep best-effort
    // data from step 1.  Not an error; just less detail.
  }
 
  // 3. Check Jamf specifically (only if not already identified).
  if (!result.mdmProvider) {
    try {
      const { stdout } = await execAsync(
        "jamf checkJSSConnection 2>/dev/null",
        {
          maxBuffer:  1024 * 1024,
          timeout:    EXEC_TIMEOUT_MS,
          killSignal: EXEC_KILL_SIGNAL,
        },
      );
      if (/successfully/i.test(stdout) || /connected/i.test(stdout)) {
        result.isEnrolled  = true;
        result.mdmProvider = "Jamf";
        const urlMatch = stdout.match(/https?:\/\/[^\s]+/);
        if (urlMatch && !result.serverUrl) result.serverUrl = urlMatch[0];
      }
    } catch {
      // jamf binary not installed — expected on most non-MDM machines
    }
  }
 
  // 4. system_profiler for supervision status
  try {
    const { stdout } = await execAsync(
      "system_profiler SPConfigurationProfileDataType -json 2>/dev/null",
      {
        maxBuffer:  5 * 1024 * 1024,
        timeout:    EXEC_TIMEOUT_MS,
        killSignal: EXEC_KILL_SIGNAL,
      },
    );
    if (stdout.trim()) {
      const parsed = JSON.parse(stdout) as {
        SPConfigurationProfileDataType?: Array<{ _supervised?: boolean }>;
      };
      const profiles = parsed.SPConfigurationProfileDataType ?? [];
      if (profiles.length > 0) {
        result.isEnrolled = true;
        result.supervised = profiles[0]._supervised ?? null;
        if (!result.mdmProvider) result.mdmProvider = "Unknown MDM";
      }
    }
  } catch {
    // system_profiler may fail or return empty — best effort
  }
 
  if (!result.isEnrolled) result.enrollmentType = "none";
  return result;
}
 
// -- win32 implementation -----------------------------------------------------
 
async function checkMdmEnrollmentWin32(): Promise<MdmEnrollmentResult> {
  const ps = `
$ErrorActionPreference = 'SilentlyContinue'
$enrollments = Get-ChildItem 'HKLM:\\SOFTWARE\\Microsoft\\Enrollments' -ErrorAction SilentlyContinue |
               Get-ItemProperty -ErrorAction SilentlyContinue |
               Select-Object UPN,ProviderID,EnrollmentState,DiscoveryServiceFullURL
if ($enrollments) { @($enrollments) | ConvertTo-Json -Depth 2 -Compress } else { '[]' }`.trim();
 
  const result = emptyResult("full");
 
  try {
    const raw    = await runPS(ps);
    const parsed = JSON.parse(raw ?? "[]") as Array<{
      UPN?:                    string;
      ProviderID?:             string;
      EnrollmentState?:        number;
      DiscoveryServiceFullURL?: string;
    }>;
 
    const active = parsed.filter((e) => e.EnrollmentState === 1);
    if (active.length > 0) {
      result.isEnrolled     = true;
      result.enrollmentType = "device_enrollment";
      result.mdmProvider    = active[0].ProviderID ?? null;
      result.serverUrl      = active[0].DiscoveryServiceFullURL ?? null;
 
      if (result.mdmProvider) {
        if (/intune|microsoft/i.test(result.mdmProvider)) result.mdmProvider = "Microsoft Intune";
        if (/jamf/i.test(result.mdmProvider))             result.mdmProvider = "Jamf";
      } else if (result.serverUrl) {
        if (/intune|microsoft/i.test(result.serverUrl)) result.mdmProvider = "Microsoft Intune";
        if (/jamf/i.test(result.serverUrl))             result.mdmProvider = "Jamf";
      }
    }
  } catch {
    // Registry access failed or PowerShell hit its internal exec timeout.
    result.source = "error";
  }
 
  return result;
}
 
// -- Top-level race -----------------------------------------------------------
//
// Defence-in-depth: even if every individual exec respects its 5 s timeout,
// the platform implementation must still settle within OVERALL_BUDGET_MS so
// the tool never trips G4's 30 s global gate.  When the race fires, downstream
// code sees `source: "timeout"` and treats the device as not-enrolled, which
// is the correct conservative default for the software-reinstall workflow.
 
function withOverallBudget<T extends MdmEnrollmentResult>(
  work:    Promise<T>,
  budget:  number,
): Promise<MdmEnrollmentResult> {
  return new Promise((resolve) => {
    let settled = false;
    const timer = setTimeout(() => {
      if (settled) return;
      settled = true;
      resolve(emptyResult("timeout"));
    }, budget);
    work.then(
      (val) => {
        if (settled) return;
        settled = true;
        clearTimeout(timer);
        resolve(val);
      },
      () => {
        if (settled) return;
        settled = true;
        clearTimeout(timer);
        resolve(emptyResult("error"));
      },
    );
  });
}
 
// -- Exported run function ----------------------------------------------------
 
export async function run(_args: Record<string, never> = {}) {
  const platform = os.platform();
  const work = platform === "win32"
    ? checkMdmEnrollmentWin32()
    : checkMdmEnrollmentDarwin();
  return withOverallBudget(work, __testing.budgetMs);
}
 
/**
 * Test-only knob — lets unit tests assert the overall-budget race fires
 * without waiting 8 s of wall-clock time.  Production code never imports
 * this; the runtime simply reads `__testing.budgetMs` once per call.
 */
export const __testing = { budgetMs: OVERALL_BUDGET_MS };
 
// -- CLI smoke test -----------------------------------------------------------
 
if (false) {
  run({})
    .then(r => console.log(JSON.stringify(r, null, 2)))
    .catch((err: Error) => { console.error(err.message); process.exit(1); });
}