Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/scripts/__tests__/codex-native-hook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6661,6 +6661,32 @@ PY`,
assert.equal(resolved, 1100);
});

it("resolves the Codex owner from Windows process lineage without Unix ps", () => {
const resolved = resolveSessionOwnerPidFromAncestry(1700, {
platform: "win32",
readProcessLineage: () => [
{
pid: 1700,
name: "powershell.exe",
command: 'powershell.exe -File "C:\\Users\\user\\.codex\\hooks\\omx-native-hook-windows-shim.ps1"',
},
{
pid: 19072,
name: "pwsh.exe",
command: 'pwsh.exe -Command "verify expected codex.exe owner"',
},
{
pid: 14440,
name: "codex.exe",
command: '"C:\\Users\\user\\AppData\\Roaming\\npm\\node_modules\\@openai\\codex\\codex.exe"',
},
{ pid: 14400, name: "node.exe", command: "node.exe codex.js" },
],
});

assert.equal(resolved, 14440);
});

it("records keyword activation from UserPromptSubmit payloads", async () => {
const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-"));
try {
Expand Down
110 changes: 99 additions & 11 deletions src/scripts/codex-native-hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1528,6 +1528,8 @@ async function readActiveRalphState(

function readParentPid(pid: number): number | null {
try {
if (process.platform === "win32") return null;

if (process.platform === "linux") {
const stat = readFileSync(`/proc/${pid}/stat`, "utf-8");
const commandEnd = stat.lastIndexOf(")");
Expand All @@ -1552,6 +1554,8 @@ function readParentPid(pid: number): number | null {

function readProcessCommand(pid: number): string {
try {
if (process.platform === "win32") return "";

if (process.platform === "linux") {
return readFileSync(`/proc/${pid}/cmdline`, "utf-8")
.replace(/\u0000+/g, " ")
Expand All @@ -1568,36 +1572,120 @@ function readProcessCommand(pid: number): string {
}
}

interface ProcessLineageEntry {
pid: number;
command: string;
name?: string;
}

interface WindowsProcessRecord {
ProcessId?: unknown;
ParentProcessId?: unknown;
CommandLine?: unknown;
Name?: unknown;
}

const WINDOWS_PROCESS_TABLE_TIMEOUT_MS = 5_000;
const WINDOWS_PROCESS_TABLE_MAX_BUFFER_BYTES = 4 * 1024 * 1024;

function readWindowsProcessLineage(startPid: number): ProcessLineageEntry[] | null {
if (!Number.isInteger(startPid) || startPid <= 1) return null;

try {
const output = execFileSync(
"powershell.exe",
[
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-Command",
"Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CommandLine,Name | ConvertTo-Json -Compress",
],
{
encoding: "utf-8",
windowsHide: true,
timeout: WINDOWS_PROCESS_TABLE_TIMEOUT_MS,
maxBuffer: WINDOWS_PROCESS_TABLE_MAX_BUFFER_BYTES,
},
);
const parsed = JSON.parse(output) as unknown;
const records = Array.isArray(parsed) ? parsed : [parsed];
const processTable = new Map<number, { parentPid: number | null; command: string; name: string }>();

for (const record of records) {
if (!record || typeof record !== "object") continue;
const processRecord = record as WindowsProcessRecord;
const pid = safePositiveInteger(processRecord.ProcessId);
const parentPid = safePositiveInteger(processRecord.ParentProcessId);
const command = safeString(processRecord.CommandLine).trim()
|| safeString(processRecord.Name).trim();
const name = safeString(processRecord.Name).trim();
if (pid === null || !command) continue;
processTable.set(pid, { parentPid, command, name });
}

const lineage: ProcessLineageEntry[] = [];
let currentPid = startPid;
for (let i = 0; i < 6 && currentPid > 1; i += 1) {
const current = processTable.get(currentPid);
if (!current) break;
lineage.push({ pid: currentPid, command: current.command, name: current.name });
if (!current.parentPid || current.parentPid === currentPid) break;
currentPid = current.parentPid;
}
return lineage.length > 0 ? lineage : null;
} catch {
return null;
}
}

function looksLikeShellCommand(command: string): boolean {
return /(^|[\/\s])(bash|zsh|sh|dash|fish|ksh)(\s|$)/i.test(command);
return /(^|[\\/\s])(bash|zsh|sh|dash|fish|ksh|powershell(?:\.exe)?|pwsh(?:\.exe)?|cmd(?:\.exe)?)(\s|$)/i.test(command);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recognize quoted Windows shell paths

On Windows the managed hook can launch PowerShell by a quoted absolute path, so Win32_Process.CommandLine may start like "C:\\...\\powershell.exe" -File ...; this pattern only treats powershell.exe/cmd.exe as a shell when whitespace or end-of-string follows the executable name. In fallback cases where no exact codex.exe/codex.js ancestor is matched, looksLikeShellCommand(lineage[0].command) then returns false and the resolver records the transient shim PID instead of lineage[1], recreating the stale-dead Stop owner on those installs.

Useful? React with 👍 / 👎.

}

function looksLikeCodexCommand(command: string): boolean {
if (/codex-native-hook(?:\.js)?/i.test(command)) return false;
return /\bcodex(?:\.js)?\b/i.test(command);
return /(^|[\\/\s"'])codex(?:\.exe|\.js)?(?=$|[\s"'])/i.test(command);
}

function looksLikeCodexProcess(entry: ProcessLineageEntry): boolean {
const name = safeString(entry.name).trim();
if (/^codex(?:\.exe)?$/i.test(name)) return true;
if (name && looksLikeShellCommand(name)) return false;
return looksLikeCodexCommand(entry.command);
}

export function resolveSessionOwnerPidFromAncestry(
startPid: number,
options: {
readParentPid?: (pid: number) => number | null;
readProcessCommand?: (pid: number) => string;
readProcessLineage?: (pid: number) => ProcessLineageEntry[] | null;
platform?: NodeJS.Platform;
} = {},
): number | null {
const readParent = options.readParentPid ?? readParentPid;
const readCommand = options.readProcessCommand ?? readProcessCommand;
const lineage: Array<{ pid: number; command: string }> = [];
let currentPid = startPid;
const platform = options.platform ?? process.platform;
let lineage: ProcessLineageEntry[] = [];

for (let i = 0; i < 6 && Number.isInteger(currentPid) && currentPid > 1; i += 1) {
const command = readCommand(currentPid);
lineage.push({ pid: currentPid, command });
const nextPid = readParent(currentPid);
if (!nextPid || nextPid === currentPid) break;
currentPid = nextPid;
if (options.readProcessLineage) {
lineage = options.readProcessLineage(startPid) ?? [];
} else if (platform === "win32" && !options.readParentPid && !options.readProcessCommand) {
lineage = readWindowsProcessLineage(startPid) ?? [];
} else {
let currentPid = startPid;
for (let i = 0; i < 6 && Number.isInteger(currentPid) && currentPid > 1; i += 1) {
const command = readCommand(currentPid);
lineage.push({ pid: currentPid, command });
const nextPid = readParent(currentPid);
if (!nextPid || nextPid === currentPid) break;
currentPid = nextPid;
}
}

const codexAncestor = lineage.find((entry) => looksLikeCodexCommand(entry.command));
const codexAncestor = lineage.find(looksLikeCodexProcess);
if (codexAncestor) return codexAncestor.pid;

if (lineage.length >= 2 && looksLikeShellCommand(lineage[0]?.command || "")) {
Expand Down
Loading