restart_process
restart_process
Terminates a process by name or PID, waits briefly, then re-launches it. Use when a process is hung or unresponsive but needs to keep running (e.g. security agent, VPN client, Finder).
Metadata
Name
restart_process
Updated
last month
Source
Risk
Medium
Requires consent
true
Affected scope
user
Code
/**
* mcp/skills/restartProcess.ts — restart_process skill
*
* Terminates a process by name or PID, waits briefly, then re-launches it.
* Use when a process is hung or unresponsive but needs to keep running
* (e.g. security agent, VPN client, Finder).
*
* Platform strategy
* -----------------
* darwin `kill -TERM {pid}` then optionally `open -a {name}` or exec launchPath
* win32 PowerShell Stop-Process then Start-Process
*
* Smoke test
* npx tsx -r dotenv/config mcp/skills/restartProcess.ts --name Finder
*/
import * as os from "os";
import { exec } from "child_process";
import { promisify } from "util";
import { z } from "zod";
import { isAgentSelf } from "./_shared/processIdentity";
const execAsync = promisify(exec);
// -- Meta ---------------------------------------------------------------------
export const meta = {
name: "restart_process",
description:
"Terminates a process by name or PID, waits briefly, then re-launches it. " +
"Use when a process is hung or unresponsive but needs to keep running " +
"(e.g. security agent, VPN client, Finder).",
riskLevel: "medium",
destructive: true,
requiresConsent: true,
supportsDryRun: false,
affectedScope: ["user"],
auditRequired: true,
schema: {
name: z
.string()
.optional()
.describe("Process name to restart (e.g. 'Finder', 'CrowdStrikeFalconSensor')"),
pid: z
.number()
.optional()
.describe("Process ID to restart. Use instead of name for precision"),
launchPath: z
.string()
.optional()
.describe("Full path to re-launch after killing. Required if process doesn't relaunch itself."),
},
} as const;
// -- Types --------------------------------------------------------------------
interface RestartResult {
killed: boolean;
relaunched: boolean;
newPid: number | null;
message: string;
}
// -- Helpers ------------------------------------------------------------------
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/** Validate a process name — only allow simple identifiers to prevent injection. */
function validateName(name: string): boolean {
return /^[a-zA-Z0-9_\-. ]+$/.test(name);
}
/** Validate a launch path — must be absolute and not contain shell metacharacters. */
function validateLaunchPath(p: string): boolean {
return nodePath.isAbsolute(p) && /^[a-zA-Z0-9_\-./: ]+$/.test(p);
}
import * as nodePath from "path";
// -- 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 ----------------------------------------------------
async function getFirstPidByName(name: string): Promise<number | null> {
try {
const { stdout } = await execAsync(
`pgrep -n -x '${name.replace(/'/g, "'\\''")}' 2>/dev/null`,
);
const pid = parseInt(stdout.trim(), 10);
return isNaN(pid) ? null : pid;
} catch {
return null;
}
}
async function restartProcessDarwin(
name: string | undefined,
pid: number | undefined,
launchPath: string | undefined,
): Promise<RestartResult> {
// Resolve PID
let targetPid = pid ?? null;
if (!targetPid && name) {
if (!validateName(name)) {
throw new Error(`[restart_process] Invalid process name: ${name}`);
}
targetPid = await getFirstPidByName(name);
}
if (!targetPid) {
return {
killed: false,
relaunched: false,
newPid: null,
message: name ? `No running process found with name: ${name}` : "No PID or name provided",
};
}
// Kill the process
let killed = false;
try {
await execAsync(`kill -TERM ${targetPid} 2>&1`);
killed = true;
} catch (err) {
return {
killed: false,
relaunched: false,
newPid: null,
message: `Failed to terminate PID ${targetPid}: ${(err as Error).message}`,
};
}
// Brief pause to allow process to clean up
await sleep(1500);
// Relaunch
if (launchPath) {
if (!validateLaunchPath(launchPath)) {
return { killed, relaunched: false, newPid: null, message: `Invalid launch path: ${launchPath}` };
}
try {
const { stdout } = await execAsync(`'${launchPath.replace(/'/g, "'\\''")}' &`);
const newPid = parseInt(stdout.trim(), 10);
return {
killed,
relaunched: true,
newPid: isNaN(newPid) ? null : newPid,
message: `Process killed and re-launched via ${launchPath}`,
};
} catch (err) {
return { killed, relaunched: false, newPid: null, message: `Killed but relaunch failed: ${(err as Error).message}` };
}
}
if (name) {
if (!validateName(name)) {
return { killed, relaunched: false, newPid: null, message: `Process killed. Invalid name for relaunch.` };
}
try {
await execAsync(`open -a '${name.replace(/'/g, "'\\''")}' 2>&1`);
await sleep(1000);
const newPid = await getFirstPidByName(name);
return {
killed,
relaunched: true,
newPid: newPid ?? null,
message: `Process killed and re-launched via 'open -a ${name}'`,
};
} catch (err) {
return {
killed,
relaunched: false,
newPid: null,
message: `Killed PID ${targetPid} but relaunch via open -a failed: ${(err as Error).message}`,
};
}
}
return {
killed,
relaunched: false,
newPid: null,
message: `Killed PID ${targetPid}. No launchPath or name provided for relaunch.`,
};
}
// -- win32 implementation -----------------------------------------------------
async function restartProcessWin32(
name: string | undefined,
pid: number | undefined,
launchPath: string | undefined,
): Promise<RestartResult> {
if (!name && !pid) {
return { killed: false, relaunched: false, newPid: null, message: "No PID or name provided" };
}
if (name && !validateName(name)) {
throw new Error(`[restart_process] Invalid process name: ${name}`);
}
const stopTarget = pid ? `-Id ${pid}` : `-Name '${name!.replace(/'/g, "''")}'`;
const stopPs = `
$ErrorActionPreference = 'Stop'
Stop-Process ${stopTarget} -Force
Write-Output 'killed'`.trim();
let killed = false;
try {
const out = await runPS(stopPs);
killed = out.includes("killed");
} catch (err) {
return {
killed: false,
relaunched: false,
newPid: null,
message: `Failed to stop process: ${(err as Error).message}`,
};
}
await sleep(1500);
if (!launchPath && !name) {
return { killed, relaunched: false, newPid: null, message: `Process killed. No launch path for relaunch.` };
}
const startCmd = launchPath
? `Start-Process -FilePath '${launchPath.replace(/'/g, "''")}' -PassThru`
: `Start-Process -FilePath '${name!.replace(/'/g, "''")}' -PassThru`;
const launchPs = `
$ErrorActionPreference = 'Stop'
$p = ${startCmd}
$p.Id`.trim();
try {
const out = await runPS(launchPs);
const newPid = parseInt(out.trim(), 10);
return {
killed,
relaunched: true,
newPid: isNaN(newPid) ? null : newPid,
message: `Process killed and re-launched`,
};
} catch (err) {
return {
killed,
relaunched: false,
newPid: null,
message: `Killed but relaunch failed: ${(err as Error).message}`,
};
}
}
// -- Exported run function ----------------------------------------------------
export async function run({
name,
pid,
launchPath,
}: {
name?: string;
pid?: number;
launchPath?: string;
} = {}) {
if (!name && !pid) {
throw new Error("[restart_process] Either name or pid must be provided");
}
// Never restart the agent itself — killing it would terminate the running
// agent before any relaunch could complete.
if (isAgentSelf(name, pid)) {
return {
killed: false, relaunched: false, newPid: null,
message: "Refused: cannot restart the AI Support Agent itself.",
};
}
const platform = os.platform();
return platform === "win32"
? restartProcessWin32(name, pid, launchPath)
: restartProcessDarwin(name, pid, launchPath);
}
// -- 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); });
}