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
11 changes: 7 additions & 4 deletions src/service/desktop-supervision.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,29 @@ export interface WindowsProcessReadOptions {
/** Called with the parent image before row validation, so partial Desktop evidence keeps its veto. */
onExecutable?: (path: string) => void;
timeoutMs?: number;
env?: NodeJS.ProcessEnv;
resolveSystemDirectory?: () => string;
}

export interface DesktopSupervisionDeps {
targetPid?: number;
platform?: NodeJS.Platform;
run?: (command: string, args: string[], timeoutMs?: number) => string;
run?: (command: string, args: string[], timeoutMs?: number, env?: NodeJS.ProcessEnv) => string;
proc?: ProcReader;
readPid?: () => number | null;
readRuntimePortPid?: () => number | null;
windowsProcesses?: (pid: number, options: Pick<WindowsProcessReadOptions, "onExecutable" | "timeoutMs">) => WindowsProcessPair | null;
env?: NodeJS.ProcessEnv;
resolveWindowsSystemDirectory?: () => string;
now?: () => number;
}

export function desktopSupervisionPaths(): { pid: string; runtimePort: string };
export const procfs: ProcReader;
export function processIdentity(pid: number, execute?: DesktopSupervisionDeps["run"], onExecutable?: (path: string) => void):
{ parent: number; executable: string } | null;
export function windowsPowerShellPath(env?: NodeJS.ProcessEnv): string;
export function windowsPowerShellPath(resolveSystemDirectory?: () => string): string;
/** Pure consistency check over loaded-module paths; throws unless exactly one well-formed
* namesake per KnownDLL shares one parent directory. Exported for tests. */
export function windowsSystemDirectoryFromSharedObjects(loaded: unknown): string;
export function windowsProcessPair(pid: number, execute?: DesktopSupervisionDeps["run"], options?: WindowsProcessReadOptions): WindowsProcessPair;
export function inspectDesktopSupervision(deps?: DesktopSupervisionDeps): SupervisionEvidence;
export function createSupervisionLatch(): { observe(evidence: SupervisionEvidence): boolean };
103 changes: 90 additions & 13 deletions src/service/desktop-supervision.mjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { execFileSync } from "node:child_process";
import { accessSync, constants, readFileSync, readlinkSync, realpathSync } from "node:fs";
import { createRequire } from "node:module";
import { homedir } from "node:os";
import { basename, dirname, join, resolve, win32 } from "node:path";

// Node >=18 also imports this module from bin/ocx.mjs; do not import TS or Bun APIs.
function run(command, args, timeout = 750) {
function run(command, args, timeout = 750, env) {
return execFileSync(command, args, {
encoding: "utf8", timeout, maxBuffer: 128 * 1024, stdio: ["ignore", "pipe", "pipe"], windowsHide: true,
encoding: "utf8", timeout, env, maxBuffer: 128 * 1024,
stdio: ["ignore", "pipe", "pipe"], windowsHide: true,
}).trim();
}

Expand Down Expand Up @@ -80,22 +82,95 @@ const WINDOWS_ACQUISITION_TIMEOUT_MS = 3_000;
const WINDOWS_MIN_ACQUISITION_MS = 250;
const WINDOWS_DESKTOP_IMAGE = "opencodex-desktop.exe";

/** Mirrors src/update/npm-invocation.mjs: plain ESM cannot reach the FFI system-directory resolver. */
export function windowsPowerShellPath(env = process.env) {
const root = env.SystemRoot ?? env.windir;
const base = root && /^[A-Za-z]:\\/.test(root.replaceAll("/", "\\")) ? root : "C:\\Windows";
return win32.join(base, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
/** Bun resolves the system directory via FFI; the Node lane derives it from loaded KnownDLL paths. */
export function windowsPowerShellPath(resolveSystemDirectory = resolveWindowsSystemDirectory) {
const system = resolveSystemDirectory();
return win32.join(system, "WindowsPowerShell", "v1.0", "powershell.exe");
}

const WINDOWS_REPORT_PATH_LIMIT = 260; // Node's module-name buffer is WCHAR[MAX_PATH] (src/debug_utils.cc).
const WINDOWS_KNOWN_DLLS = ["kernel32.dll", "ntdll.dll"];

// A resolved system directory is stable for the process lifetime; only success is
// memoized, so failures keep the existing retry-on-next-call recovery contract.
let windowsSystemDirectoryCache = null;

function resolveWindowsSystemDirectory() {
if (process.platform !== "win32") throw new Error("trusted Windows resolver unavailable");
if (windowsSystemDirectoryCache !== null) return windowsSystemDirectoryCache;
const directory = process.versions.bun ? resolveWindowsSystemDirectoryViaFfi() : resolveWindowsSystemDirectoryViaSharedObjects();
return (windowsSystemDirectoryCache = directory);
}

function resolveWindowsSystemDirectoryViaFfi() {
const { dlopen, ptr } = createRequire(import.meta.url)("bun:ffi");
const lib = dlopen("kernel32.dll", { GetSystemDirectoryW: { args: ["ptr", "u32"], returns: "u32" } });
try {
let size = 260;
for (let attempt = 0; attempt < 4; attempt++) {
const buffer = new Uint16Array(size);
const written = lib.symbols.GetSystemDirectoryW(ptr(buffer.buffer), size);
if (written === 0) throw new Error("GetSystemDirectoryW failed");
if (written >= size) { size = written + 1; continue; }
return String.fromCharCode(...buffer.subarray(0, written)).replace(/[/\\]+$/, "");
}
throw new Error("Windows system directory is too long");
} finally {
lib.close();
}
}

/** Pure consistency check over loaded-module paths — no env, no filesystem. Exported for tests. */
export function windowsSystemDirectoryFromSharedObjects(loaded) {
if (!Array.isArray(loaded)) throw new Error("trusted Windows resolver unavailable");
const parents = new Map(); // lowercased -> first-seen casing (kernel32 is processed first)
for (const known of WINDOWS_KNOWN_DLLS) {
// Collect every raw namesake first: a malformed or truncated namesake must
// cause rejection, not disappear behind a format filter.
const namesakes = loaded.filter(path => typeof path === "string" && win32.basename(path).toLowerCase() === known);
for (const candidate of namesakes) {
if (candidate.length >= WINDOWS_REPORT_PATH_LIMIT || !/^[A-Za-z]:[\\/]/.test(candidate))
throw new Error("trusted Windows resolver unavailable");
}
if (namesakes.length !== 1) throw new Error("trusted Windows resolver unavailable");
const parent = win32.dirname(win32.normalize(namesakes[0]));
if (!parents.has(parent.toLowerCase())) parents.set(parent.toLowerCase(), parent);
}
if (parents.size !== 1) throw new Error("trusted Windows resolver unavailable");
return [...parents.values()][0];
}

// The report object carries environment secrets: read .sharedObjects only — never
// log, serialize, store or pass it on. Existence checks stay outside the pure validator.
// An unavailable or ambiguous report stays the bounded unknown residual.
function resolveWindowsSystemDirectoryViaSharedObjects() {
const loaded = process.report?.getReport?.()?.sharedObjects;
const directory = windowsSystemDirectoryFromSharedObjects(loaded);
for (const required of [...WINDOWS_KNOWN_DLLS, win32.join("WindowsPowerShell", "v1.0", "powershell.exe")]) {
try { accessSync(win32.join(directory, required), constants.F_OK); }
catch { throw new Error("trusted Windows resolver unavailable"); }
}
return directory;
}

function windowsPowerShellEnv(systemDirectory) {
const root = win32.dirname(systemDirectory);
return { SystemRoot: root, WINDIR: root, PATH: systemDirectory, PATHEXT: ".COM;.EXE;.BAT;.CMD" };
}

function windowsProcessScript(pid) {
// pid is a validated safe integer; it only ever reaches a numeric WQL filter.
return [
"$ErrorActionPreference='Stop'",
"$PSModuleAutoLoadingPreference='None'",
// Utility loads first: the CimCmdlets manifest needs Set-Alias, which autoload 'None' cannot supply.
"Import-Module ([IO.Path]::Combine($PSHOME,'Modules','Microsoft.PowerShell.Utility','Microsoft.PowerShell.Utility.psd1')) -Force",
"Import-Module ([IO.Path]::Combine($PSHOME,'Modules','CimCmdlets','CimCmdlets.psd1')) -Force",
"function Row($p) { if ($null -eq $p) { return $null }; [ordered]@{ pid=[string]$p.ProcessId; parent=[string]$p.ParentProcessId; exe=$p.ExecutablePath; session=[string]$p.SessionId; created=$(if ($p.CreationDate) { [string]$p.CreationDate.ToUniversalTime().Ticks } else { $null }) } }",
`$c=Get-CimInstance -ClassName Win32_Process -Filter 'ProcessId=${pid}'`,
`$c=CimCmdlets\\Get-CimInstance -ClassName Win32_Process -Filter 'ProcessId=${pid}'`,
"$p=$null",
"if ($c -and [uint32]$c.ParentProcessId -gt 0) { $p=Get-CimInstance -ClassName Win32_Process -Filter ('ProcessId=' + [uint32]$c.ParentProcessId) }",
"[ordered]@{ child=(Row $c); parent=(Row $p) } | ConvertTo-Json -Compress",
"if ($c -and [uint32]$c.ParentProcessId -gt 0) { $p=CimCmdlets\\Get-CimInstance -ClassName Win32_Process -Filter ('ProcessId=' + [uint32]$c.ParentProcessId) }",
"[ordered]@{ child=(Row $c); parent=(Row $p) } | Microsoft.PowerShell.Utility\\ConvertTo-Json -Compress",
].join("; ");
}

Expand All @@ -120,9 +195,11 @@ function windowsRow(row) {
/** One bounded Win32_Process read of a process and its parent. */
export function windowsProcessPair(pid, execute = run, options = {}) {
if (!validPid(pid)) throw new Error("invalid pid");
const output = execute(windowsPowerShellPath(options.env), [
const resolveSystemDirectory = options.resolveSystemDirectory ?? resolveWindowsSystemDirectory;
const systemDirectory = resolveSystemDirectory();
const output = execute(windowsPowerShellPath(() => systemDirectory), [
"-NoProfile", "-NoLogo", "-NonInteractive", "-Command", windowsProcessScript(pid),
], options.timeoutMs ?? WINDOWS_ACQUISITION_TIMEOUT_MS);
], options.timeoutMs ?? WINDOWS_ACQUISITION_TIMEOUT_MS, windowsPowerShellEnv(systemDirectory));
const raw = JSON.parse(output);
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("invalid process pair");
// A Desktop parent seen before a later field fails validation must keep its veto.
Expand Down Expand Up @@ -154,7 +231,7 @@ export function inspectDesktopSupervision(deps = {}) {
const now = deps.now ?? Date.now;
const deadline = now() + WINDOWS_INSPECTION_BUDGET_MS;
const readWindowsPair = deps.windowsProcesses
?? ((pid, options) => windowsProcessPair(pid, execute, { ...options, env: deps.env }));
?? ((pid, options) => windowsProcessPair(pid, execute, { ...options, resolveSystemDirectory: deps.resolveWindowsSystemDirectory }));
function identity(pid, parent = false) {
if (platform === "darwin") return processIdentity(pid, execute, parent ? seen : undefined);
const executable = proc.exe(pid);
Expand Down
22 changes: 20 additions & 2 deletions structure/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ Background service, docs, release, and design discipline.
| [`desktop-terminal-command.md`](desktop-terminal-command.md) | Desktop-owned ocx command on PATH: record and journal, POSIX shim and shell blocks, Windows user Path, private-path ACL policy, and the local Terminal command page. |
| [`desktop-start-at-login.md`](desktop-start-at-login.md) | macOS tray Start at Login judgment shared with Startup safety, its parity fixture, click rules, and the guarded launch-origin migration. |
| [`ops/service-and-sidecars.md`](ops/service-and-sidecars.md) | Service install/repair, platform launchers, tray, and sidecar processes. |
| [`ops/desktop-supervision.md`](ops/desktop-supervision.md) | Durable Desktop ownership read, takeover-consent handling, live-supervision evidence, the Windows probe's trusted system-directory acquisition, and the supervision latch blocking contract consumed by service and update guards. |
| [`ops/service-logging.md`](ops/service-logging.md) | Service-only console timestamps and bounded copy-truncate log retention. |
| [`ops/service-health-recovery.md`](ops/service-health-recovery.md) | Opt-in sentinel that turns a hung but listening managed service proxy into an exit its manager restarts. |
| [`ops/update-supervised-recovery.md`](ops/update-supervised-recovery.md) | Manager-first recovery after a failed post-update service refresh, and the non-zero unsupervised fallback. |
Expand All @@ -155,13 +156,20 @@ A source area can be described by more than one doc, because these docs are orga
| `.github/` | [`ops/docs-and-release.md`](ops/docs-and-release.md) |
| `app/` | [`overview.md`](overview.md)<br>[`companion.md`](companion.md) |
| `bin/` | [`runtime.md`](runtime.md)<br>[`ops/docs-and-release.md`](ops/docs-and-release.md) |
| `bin/ocx.mjs` | [`ops/desktop-supervision.md`](ops/desktop-supervision.md) |
| `desktop/` | [`desktop-shell.md`](desktop-shell.md)<br>[`companion.md`](companion.md) |
| `desktop/src-tauri/src/claim.rs` | [`ops/desktop-supervision.md`](ops/desktop-supervision.md) |
| `desktop/src-tauri/src/cli_command.rs` | [`desktop-terminal-command.md`](desktop-terminal-command.md) |
| `desktop/src-tauri/src/cli_command_posix.rs` | [`desktop-terminal-command.md`](desktop-terminal-command.md) |
| `desktop/src-tauri/src/cli_command_record.rs` | [`desktop-terminal-command.md`](desktop-terminal-command.md) |
| `desktop/src-tauri/src/cli_command_windows.rs` | [`desktop-terminal-command.md`](desktop-terminal-command.md) |
| `desktop/src-tauri/src/first_run.rs` | [`desktop-start-at-login.md`](desktop-start-at-login.md) |
| `desktop/src-tauri/src/first_run.rs` | [`desktop-start-at-login.md`](desktop-start-at-login.md)<br>[`ops/desktop-supervision.md`](ops/desktop-supervision.md) |
| `desktop/src-tauri/src/identity.rs` | [`ops/desktop-supervision.md`](ops/desktop-supervision.md) |
| `desktop/src-tauri/src/login_registration.rs` | [`desktop-start-at-login.md`](desktop-start-at-login.md) |
| `desktop/src-tauri/src/ownership.rs` | [`ops/desktop-supervision.md`](ops/desktop-supervision.md) |
| `desktop/src-tauri/src/resolve.rs` | [`ops/desktop-supervision.md`](ops/desktop-supervision.md) |
| `desktop/src-tauri/src/runtime_stop.rs` | [`ops/desktop-supervision.md`](ops/desktop-supervision.md) |
| `desktop/src-tauri/src/startup.rs` | [`ops/desktop-supervision.md`](ops/desktop-supervision.md) |
| `desktop/src-tauri/src/tray_login.rs` | [`desktop-start-at-login.md`](desktop-start-at-login.md) |
| `desktop/ui/cli.html` | [`desktop-terminal-command.md`](desktop-terminal-command.md) |
| `desktop/ui/cli.js` | [`desktop-terminal-command.md`](desktop-terminal-command.md) |
Expand Down Expand Up @@ -196,9 +204,11 @@ A source area can be described by more than one doc, because these docs are orga
| `src/cli/` | [`runtime.md`](runtime.md)<br>[`config.md`](config.md)<br>[`remote-messaging.md`](remote-messaging.md)<br>[`local-messaging.md`](local-messaging.md)<br>[`cli-management.md`](cli-management.md)<br>[`provider-completion-diagnostic.md`](provider-completion-diagnostic.md)<br>[`fallback-policy.md`](fallback-policy.md)<br>[`clients/integrations.md`](clients/integrations.md)<br>[`clients/codex-desktop.md`](clients/codex-desktop.md)<br>[`clients/chatgpt-desktop.md`](clients/chatgpt-desktop.md)<br>[`clients/claude-desktop.md`](clients/claude-desktop.md)<br>[`clients/hermes.md`](clients/hermes.md)<br>[`ops/docs-and-release.md`](ops/docs-and-release.md) |
| `src/cli/account-preservation.ts` | [`providers/openai-signed-in-preservation.md`](providers/openai-signed-in-preservation.md) |
| `src/cli/codex-provider.ts` | [`codex-provider-routing.md`](codex-provider-routing.md) |
| `src/cli/desktop-runtime-guidance.ts` | [`ops/desktop-supervision.md`](ops/desktop-supervision.md) |
| `src/cli/link-apply-restart.ts` | [`remote-link.md`](remote-link.md) |
| `src/cli/models-runtime.ts` | [`pricing.md`](pricing.md) |
| `src/cli/status-codex-settings.ts` | [`remote-link.md`](remote-link.md) |
| `src/cli/update-restart.ts` | [`ops/desktop-supervision.md`](ops/desktop-supervision.md) |
| `src/client/` | [`runtime.md`](runtime.md)<br>[`fallback-policy.md`](fallback-policy.md)<br>[`clients/claude-desktop.md`](clients/claude-desktop.md) |
| `src/client/link-home.ts` | [`remote-link.md`](remote-link.md) |
| `src/client/link-parity-reporter.ts` | [`remote-link.md`](remote-link.md) |
Expand Down Expand Up @@ -310,14 +320,20 @@ A source area can be described by more than one doc, because these docs are orga
| `src/service-manager-probe.ts` | [`ops/service-and-sidecars.md`](ops/service-and-sidecars.md) |
| `src/service.ts` | [`runtime.md`](runtime.md)<br>[`ops/docs-and-release.md`](ops/docs-and-release.md) |
| `src/service/` | [`runtime.md`](runtime.md) |
| `src/service/desktop-startup.ts` | [`desktop-start-at-login.md`](desktop-start-at-login.md) |
| `src/service/cli.ts` | [`ops/desktop-supervision.md`](ops/desktop-supervision.md) |
| `src/service/desktop-command-guard.ts` | [`ops/desktop-supervision.md`](ops/desktop-supervision.md) |
| `src/service/desktop-startup.ts` | [`desktop-start-at-login.md`](desktop-start-at-login.md)<br>[`ops/desktop-supervision.md`](ops/desktop-supervision.md) |
| `src/service/desktop-supervision.d.mts` | [`ops/desktop-supervision.md`](ops/desktop-supervision.md) |
| `src/service/desktop-supervision.mjs` | [`ops/desktop-supervision.md`](ops/desktop-supervision.md) |
| `src/service/health-sentinel-launch.ts` | [`ops/service-health-recovery.md`](ops/service-health-recovery.md) |
| `src/service/health-sentinel-linux.ts` | [`ops/service-health-recovery.md`](ops/service-health-recovery.md) |
| `src/service/health-sentinel-main.ts` | [`ops/service-health-recovery.md`](ops/service-health-recovery.md) |
| `src/service/health-sentinel-policy.ts` | [`ops/service-health-recovery.md`](ops/service-health-recovery.md) |
| `src/service/health-sentinel-probe.ts` | [`ops/service-health-recovery.md`](ops/service-health-recovery.md) |
| `src/service/health-sentinel-windows.ts` | [`ops/service-health-recovery.md`](ops/service-health-recovery.md) |
| `src/service/logging.ts` | [`ops/service-logging.md`](ops/service-logging.md) |
| `src/service/orchestration.ts` | [`ops/desktop-supervision.md`](ops/desktop-supervision.md) |
| `src/service/repair.ts` | [`ops/desktop-supervision.md`](ops/desktop-supervision.md) |
| `src/sidecar/` | [`ops/service-and-sidecars.md`](ops/service-and-sidecars.md) |
| `src/sidecar/anthropic-binding.ts` | [`providers/anthropic-account-pool.md`](providers/anthropic-account-pool.md) |
| `src/stall-timeout.ts` | [`runtime.md`](runtime.md) |
Expand All @@ -328,8 +344,10 @@ A source area can be described by more than one doc, because these docs are orga
| `src/types/account-auto-pause.ts` | [`account-auto-pause.md`](account-auto-pause.md) |
| `src/types/provider.ts` | [`pricing.md`](pricing.md) |
| `src/update/` | [`runtime.md`](runtime.md)<br>[`ops/service-and-sidecars.md`](ops/service-and-sidecars.md) |
| `src/update/index.ts` | [`ops/desktop-supervision.md`](ops/desktop-supervision.md) |
| `src/update/job-supervised-restart.ts` | [`ops/update-supervised-recovery.md`](ops/update-supervised-recovery.md) |
| `src/update/job.ts` | [`ops/update-supervised-recovery.md`](ops/update-supervised-recovery.md) |
| `src/update/restart-ownership.ts` | [`ops/desktop-supervision.md`](ops/desktop-supervision.md) |
| `src/update/supervised-recovery.mjs` | [`ops/update-supervised-recovery.md`](ops/update-supervised-recovery.md) |
| `src/usage/` | [`runtime.md`](runtime.md)<br>[`providers/jev-decision.md`](providers/jev-decision.md)<br>[`gui-and-management-api.md`](gui-and-management-api.md)<br>[`dashboard-and-usage.md`](dashboard-and-usage.md)<br>[`companion.md`](companion.md) |
| `src/usage/cost.ts` | [`pricing.md`](pricing.md) |
Expand Down
Loading
Loading