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
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); });
}
``