run_installer

run_installer

Runs a downloaded installer file (.pkg/.dmg on macOS; .msi/.exe on Windows) to (re)install a software application. Use after download_installer when the user has confirmed they want to proceed with installation. Requires admin privileges, which the privileged helper daemon supplies for non-admin users.

Metadata

Name

run_installer

Updated

3 weeks ago

Source

GitHub source

Risk

High

Requires consent

true

Affected scope

system

Code

/**
 * mcp/skills/runInstaller.ts — run_installer skill
 *
 * Executes a downloaded installer file (.pkg/.dmg on macOS;
 * .msi/.exe on Windows) to (re)install a software application.
 * Use after `download_installer` (Skill #8 Step 6) when the user has
 * confirmed they want to proceed with installation.
 *
 * Privilege model
 * ---------------
 * Installation requires admin / LocalSystem privilege to write into
 * /Applications, /Library/, C:\Program Files, etc.  The agent runs as
 * the standard user; G4 routes this tool through the privileged helper
 * daemon (Workstream B v2 + fast-follow) so non-admin users complete
 * the install end-to-end without an interactive password prompt.  When
 * the helper is unavailable (HELPER_DAEMON_ENABLED=false / not
 * installed / unreachable), the call denies with helper-error /
 * helper-unavailable / scope-boundary and the agent falls back to the
 * "ask the user to run the installer manually" path.
 *
 * Platform strategy
 * -----------------
 * macOS .pkg   `installer -pkg <path> -target /`
 * macOS .dmg   mount via `hdiutil`, copy .app to /Applications, eject
 * Windows .msi `msiexec /i <path> /qn /norestart`
 * Windows .exe Start-Process with /S silent flag
 *
 * Smoke test
 *   npx tsx -r dotenv/config mcp/skills/runInstaller.ts
 */

import * as fs        from "fs";
import * as path      from "path";
import * as os        from "os";
import { z }          from "zod";

// -- Meta ---------------------------------------------------------------------

export const meta = {
  name: "run_installer",
  description:
    "Runs a downloaded installer file (.pkg/.dmg on macOS; .msi/.exe on " +
    "Windows) to (re)install a software application.  Use after " +
    "download_installer when the user has confirmed they want to " +
    "proceed with installation.  Requires admin privileges, which the " +
    "privileged helper daemon supplies for non-admin users.",
  riskLevel:       "high",
  destructive:     true,
  requiresConsent: true,
  supportsDryRun:  true,
  affectedScope:   ["system"],
  auditRequired:   true,
  escalationHint:  {
    darwin:
      "sudo installer -pkg <path>.pkg -target /  # for .pkg; .dmg requires hdiutil mount + cp + eject",
    win32:
      "msiexec /i <path>.msi /qn /norestart  # for .msi; .exe varies per vendor (try /S for silent install)",
  },
  schema: {
    installer_path: z
      .string()
      .min(1)
      .describe(
        "Absolute path to the installer file on disk.  Typically the " +
        "filePath returned by a prior download_installer call.",
      ),
    installer_type: z
      .enum(["pkg", "dmg", "msi", "exe"])
      .optional()
      .describe(
        "Installer type.  When omitted, auto-detected from the file " +
        "extension.  Must match the platform (pkg/dmg → macOS; " +
        "msi/exe → Windows).",
      ),
    dryRun: z
      .boolean()
      .optional()
      .describe(
        "If true, validate the installer + show what would run, but do " +
        "not execute.  Default: true (G4 dry-run-first policy).",
      ),
  },
} as const;

// -- Types --------------------------------------------------------------------

interface RunInstallerResult {
  installerPath: string;
  installerType: "pkg" | "dmg" | "msi" | "exe";
  dryRun:        boolean;
  /** The exact command string the helper / sudo would execute.  Echoed
   *  in dry-run mode so the consent gate can show it to the user. */
  plannedCommand: string;
  /** Set after a real run; absent in dry-run mode. */
  exitCode?:     number;
  /** Set after a real run; absent in dry-run mode. */
  durationMs?:   number;
  message:       string;
}

// -- Helpers ------------------------------------------------------------------

function detectInstallerType(installerPath: string): "pkg" | "dmg" | "msi" | "exe" {
  const ext = path.extname(installerPath).toLowerCase().replace(/^\./, "");
  if (ext === "pkg" || ext === "dmg" || ext === "msi" || ext === "exe") {
    return ext;
  }
  throw new Error(
    `Cannot detect installer type from extension '.${ext}' — supply installerType explicitly`,
  );
}

function plannedCommandFor(
  type: "pkg" | "dmg" | "msi" | "exe",
  installerPath: string,
): string {
  switch (type) {
    case "pkg":
      return `installer -pkg \