renew_kerberos_ticket

renew_kerberos_ticket

Renews the endpoint's Kerberos ticket-granting ticket (TGT) without prompting for a password. On macOS uses kinit -R; on Windows uses klist purge + gpupdate /force. Never accepts a password parameter — if interactive authentication is required, returns a clear 'interactive' status and asks the user to run kinit in their terminal.

Metadata

Name

renew_kerberos_ticket

Updated

2 months ago

Source

GitHub source

Risk

Medium

Requires consent

true

Affected scope

user

Code

/**
 * mcp/skills/renewKerberosTicket.ts — renew_kerberos_ticket
 *
 * Renews or refreshes the user's Kerberos ticket-granting ticket (TGT)
 * after check_kerberos_ticket reports the ticket expired or expiring.
 *
 * Platform strategy
 * -----------------
 * darwin  `kinit -R` first (silent renewal of an existing TGT; preferred
 *         because no password prompt).  If that fails, we DO NOT fall
 *         back to `kinit <principal>` — that prompts for a password on
 *         stdin, which we refuse to handle in-agent.  Instead we return
 *         an "interactive" status and ask the user to run kinit
 *         themselves in their terminal.
 * win32   `klist purge` then `gpupdate /force` — purging the cache
 *         triggers Windows to re-acquire a TGT on the next operation
 *         that needs one, using the cached user credentials.
 *
 * Never handles passwords.  The destructive=false classification
 * reflects the fact that renewals are reversible (a new TGT can always
 * be acquired).  requiresConsent because repeated kinit failures can
 * lock a domain account.
 */

import { z } from "zod";

import { execAsync, isDarwin, isWin32 } from "./_shared/platform";

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

export const meta = {
  name: "renew_kerberos_ticket",
  description:
    "Renews the endpoint's Kerberos ticket-granting ticket (TGT) without " +
    "prompting for a password. On macOS uses `kinit -R`; on Windows uses " +
    "`klist purge` + `gpupdate /force`. Never accepts a password parameter " +
    "— if interactive authentication is required, returns a clear " +
    "'interactive' status and asks the user to run kinit in their terminal.",
  riskLevel:       "medium",
  destructive:     false,
  requiresConsent: true,
  supportsDryRun:  true,
  affectedScope:   ["user"],
  auditRequired:   true,
  escalationHint:  {
    win32: "gpupdate /force  # run from elevated Command Prompt; refreshes Group Policy and re-issues Kerberos tickets",
  },
  schema: {
    dryRun: z
      .boolean()
      .optional()
      .describe("When true, report the command that would run without executing."),
  },
} as const;

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

export interface RenewResult {
  platform: "darwin" | "win32" | "other";
  command:  string;
  dryRun:   boolean;
  status:   "renewed" | "interactive" | "failed" | "unsupported";
  stdout?:  string;
  error?:   string;
  message:  string;
}

// -- Implementation -----------------------------------------------------------

async function renewDarwin(dryRun: boolean): Promise<RenewResult> {
  const command = `kinit -R`;
  if (dryRun) {
    return {
      platform: "darwin", command, dryRun: true, status: "renewed",
      message: `Would run \\`${command}\\` to renew the existing TGT.`,
    };
  }
  try {
    const { stdout } = await execAsync(command, {
      maxBuffer: 1 * 1024 * 1024, timeout: 10_000,
    });
    return {
      platform: "darwin", command, dryRun: false, status: "renewed",
      stdout,
      message: "Kerberos ticket renewed successfully.",
    };
  } catch (err) {
    const msg = (err as Error).message;
    const classification = classifyKinitError(msg);
    if (classification === "interactive") {
      return {
        platform: "darwin", command, dryRun: false, status: "interactive",
        error: msg,
        message:
          "Existing ticket is not renewable (missing or expired). " +
          "Open a terminal and run `kinit <your-principal>` to obtain a " +
          "fresh TGT — the agent will not handle your password.",
      };
    }
    return {
      platform: "darwin", command, dryRun: false, status: "failed",
      error: msg,
      message: `kinit -R failed: ${msg}`,
    };
  }
}

async function renewWin32(dryRun: boolean): Promise<RenewResult> {
  const command = `klist purge && gpupdate /force`;
  if (dryRun) {
    return {
      platform: "win32", command, dryRun: true, status: "renewed",
      message: `Would run \\`klist purge\\` then \\`gpupdate /force\\` to trigger TGT refresh.`,
    };
  }
  try {
    const purge = await execAsync(`klist purge`, {
      maxBuffer: 1 * 1024 * 1024, timeout: 10_000,
    });
    const update = await execAsync(`gpupdate /force`, {
      maxBuffer: 1 * 1024 * 1024, timeout: 30_000,
    });
    return {
      platform: "win32", command, dryRun: false, status: "renewed",
      stdout: `${purge.stdout}\n${update.stdout}`,
      message: "Kerberos ticket cache purged and Group Policy refresh triggered.",
    };
  } catch (err) {
    const msg = (err as Error).message;
    return {
      platform: "win32", command, dryRun: false, status: "failed",
      error: msg,
      message: `Kerberos refresh failed: ${msg}`,
    };
  }
}

/**
 * Classifies a raw kinit -R error message as "interactive" (no renewable
 * credential → user must run kinit manually) vs "failed" (any other
 * error).  Extracted so the branch logic can be unit-tested without
 * mocking rejected promises (vitest 4 flags those as unhandled
 * rejections even when caught).
 */
export function classifyKinitError(msg: string): "interactive" | "failed" {
  return /renewable|No credentials|cache|expired/i.test(msg) ? "interactive" : "failed";
}

// Exported for unit tests.
export const __testing = { renewDarwin, renewWin32 };

// -- Exported run function ----------------------------------------------------

export async function run({
  dryRun = false,
}: {
  dryRun?: boolean;
} = {}): Promise<RenewResult> {
  const platform: "darwin" | "win32" | "other" =
    isDarwin() ? "darwin" : isWin32() ? "win32" : "other";

if (platform === "darwin") return renewDarwin(dryRun);
  if (platform === "win32")  return renewWin32(dryRun);

return {
    platform: "other", command: "(unsupported)", dryRun, status: "unsupported",
    message: "Unsupported platform — Kerberos tools not available.",
  };
}

// -- CLI smoke test -----------------------------------------------------------

if (false) {
  run({ dryRun: true })
    .then((r) => console.log(JSON.stringify(r, null, 2)))
    .catch((err: Error) => { console.error(err.message); process.exit(1); });
}
``