Skip to content
121 changes: 115 additions & 6 deletions src/service-manager-probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ import {
} from "./lib/windows-elevation";
import { decodeWindowsTextBytes } from "./lib/windows-text";
import { WINSW_SERVICE_ID } from "./lib/winsw";
import { BUN_RUNTIME_PATH_ENV, BUN_RUNTIME_SOURCE_ENV } from "./lib/bun-runtime";
import { WINDOWS_WRAPPER_PROTOCOL_ENV, WINDOWS_WRAPPER_STAY_OUT_EXIT_CODE } from "./service/windows-wrapper-exit";
import { buildWindowsServiceScript, windowsTaskActionMatches } from "./service/windows-taskxml";
import { inspectServiceStateEvidence, serviceStatePathsForOpenCodexHome } from "./service/state";

/** Short: this runs inside admission, and a slow answer is the same as none. */
export const SERVICE_PROBE_TIMEOUT_MS = 2_000;
Expand Down Expand Up @@ -192,6 +196,8 @@ export interface ProbeDeps {
readonly windowsLocale?: string;
/** Startup-local full-listing cache; targeted task queries always bypass it. */
readonly windowsTaskListingCache?: WindowsTaskListingCache;
/** Service-state evidence paths; production derives them from the effective config home. */
readonly statePaths?: readonly string[];
}

const LABEL = "com.opencodex.proxy";
Expand Down Expand Up @@ -559,9 +565,95 @@ function decodeBatchPathValue(
.replaceAll(escapedPercent, "%");
}

/** Validate the generated wrapper before interpreting omitted optional homes. */
function wrapperLooksGenerated(body: string): boolean {
return /:loop\s*[\s\S]*^"%OCX_BUN%" "%OCX_CLI%" start\b[^\r\n]*$/im.test(body);
/** One generated quoted assignment, rejecting unquoted and duplicate forms. */
function generatedBatchSetValue(body: string, name: string): string | null {
const assignments = body.split(/\r?\n/).filter(line => new RegExp(`^\\s*@?set\\s+"?${name}=`, "i").test(line));
return assignments.length === 1 ? batchSetValue(assignments[0]!, name) : null;
}

/** Compare executable lines with the actual standalone generator's ordered script. */
function matchesGeneratedStandaloneControlFlow(body: string, port: number): boolean {
const scriptLines = (script: string): string[] => {
const lines = script.replace(/\r\n/g, "\n").split("\n");
if (lines.at(-1) === "") lines.pop();
return lines;
};
const lines = scriptLines(body);
const expected = scriptLines(buildWindowsServiceScript({
bun: "C:\\OpenCodex\\ocx.exe", bunRuntimeSource: "standalone", cli: null,
}, port, []));
const tokenBlock = 'if exist "%OCX_API_TOKEN_FILE%" (';
const boundary = lines.indexOf(tokenBlock);
const expectedBoundary = expected.indexOf(tokenBlock);
if (boundary < 0 || expectedBoundary < 0) return false;
const allowed = [
"OCX_SERVICE", WINDOWS_WRAPPER_PROTOCOL_ENV, BUN_RUNTIME_SOURCE_ENV,
BUN_RUNTIME_PATH_ENV, "PATH", "CODEX_HOME", "CODEX_SQLITE_HOME",
"OPENCODEX_HOME", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY",
"OCX_API_TOKEN_FILE", "OCX_SERVICE_LOG", "OCX_BUN",
];
const required = [
"OCX_SERVICE", WINDOWS_WRAPPER_PROTOCOL_ENV, BUN_RUNTIME_SOURCE_ENV,
BUN_RUNTIME_PATH_ENV, "OCX_API_TOKEN_FILE",
"OCX_SERVICE_LOG", "OCX_BUN",
];
let previous = -1;
const seen = new Set<string>();
for (const line of lines.slice(0, boundary)) {
if (line === 'set "ERRORLEVEL="') continue;
if (!line.startsWith('set "')) continue;
const name = /^set "([A-Z_]+)=[^"\r\n]*"$/.exec(line)?.[1];
const index = name ? allowed.indexOf(name) : -1;
if (index <= previous || !name) return false;
previous = index;
seen.add(name);
}
if (required.some(name => !seen.has(name))) return false;
const withoutPrefixSets = (scriptLines: string[], end: number): string[] =>
scriptLines.filter((line, index) => index >= end || line === 'set "ERRORLEVEL="' || !line.startsWith('set "'));
const actualFlow = withoutPrefixSets(lines, boundary);
const generatedFlow = withoutPrefixSets(expected, expectedBoundary);
return actualFlow.length === generatedFlow.length
&& actualFlow.every((line, index) => line === generatedFlow[index]);
}

/** Validate the generated launch shape before interpreting omitted optional homes. */
function wrapperLaunchShape(body: string): "source" | { standaloneBun: string } | null {
if (!/^:loop\s*$/im.test(body)) return null;
const launchLines = body.split(/\r?\n/).filter(line => /^\s*"%OCX_BUN%"/i.test(line));
if (launchLines.length !== 1) return null;
const launch = launchLines[0]!.trim();
const sourceLaunch = /^"%OCX_BUN%" "%OCX_CLI%" start --port ([0-9]{1,5}) >>"%OCX_SERVICE_LOG%" 2>&1$/i;
const standaloneLaunch = /^"%OCX_BUN%" start --port ([0-9]{1,5}) >>"%OCX_SERVICE_LOG%" 2>&1$/i;
const bun = generatedBatchSetValue(body, "OCX_BUN");
if (!bun) return null;
const cliAssignments = body.split(/\r?\n/).filter(line => /^\s*@?set\s+"?OCX_CLI=/i.test(line));
if (cliAssignments.length > 0) {
const port = sourceLaunch.exec(launch)?.[1];
return cliAssignments.length === 1 && Boolean(generatedBatchSetValue(body, "OCX_CLI"))
&& Boolean(port) && Number(port) >= 1 && Number(port) <= 65535
? "source" : null;
}
const port = standaloneLaunch.exec(launch)?.[1];
if (!port || Number(port) < 1 || Number(port) > 65535 || !/^@echo off\s*$/im.test(body)
|| !/^setlocal EnableExtensions DisableDelayedExpansion\s*$/im.test(body)
|| !new RegExp(`^if "%ERRORLEVEL%"=="${WINDOWS_WRAPPER_STAY_OUT_EXIT_CODE}" goto stopped\\s*$`, "im").test(body)
|| !/^:stopped\s*$/im.test(body)) return null;
if (!matchesGeneratedStandaloneControlFlow(body, Number(port))) return null;
for (const [name, value] of [
["OCX_SERVICE", "1"],
[WINDOWS_WRAPPER_PROTOCOL_ENV, "1"],
[BUN_RUNTIME_SOURCE_ENV, "standalone"],
]) {
if (generatedBatchSetValue(body, name) !== value) return null;
}
const runtimePath = generatedBatchSetValue(body, BUN_RUNTIME_PATH_ENV);
if (!bun || !runtimePath || normalizeWindowsPath(decodeBatchPathValue(bun)) !== normalizeWindowsPath(decodeBatchPathValue(runtimePath))) return null;
for (const name of ["CODEX_HOME", "OPENCODEX_HOME"]) {
const assignments = body.split(/\r?\n/).filter(line => new RegExp(`^\\s*@?set\\s+"?${name}=`, "i").test(line));
if (assignments.length > 0 && generatedBatchSetValue(body, name) === null) return null;
}
return { standaloneBun: decodeBatchPathValue(bun) };
}

function normalizeWindowsPath(value: string): string {
Expand Down Expand Up @@ -711,7 +803,7 @@ function probeWinswRegistration(

function inspectWindows(
deps: Required<Pick<ProbeDeps, "runRaw" | "home">>
& Pick<ProbeDeps, "configDir" | "winswStatus" | "windowsLocale" | "windowsTaskListingCache">,
& Pick<ProbeDeps, "configDir" | "winswStatus" | "windowsLocale" | "windowsTaskListingCache" | "statePaths">,
): ServiceManagerInstallation {
const configDir = windowsConfigDirPath(deps);
const taskXmlPath = join(configDir, "opencodex-service-task.xml");
Expand Down Expand Up @@ -802,6 +894,10 @@ function inspectWindows(
const registeredWalk = walkWindowsChain(deps, registration.registeredXml, taskXmlPath);
if (registeredWalk.kind !== "present") return registeredWalk;
const registeredClaim = registeredWalk.claims[0];
const registeredLauncher = /"([^"]+)"/.exec(windowsTaskArguments(registration.registeredXml) ?? "")?.[1];
if (!registeredLauncher || !windowsTaskActionMatches(registration.registeredXml, registeredLauncher)) {
return unknown("the registered scheduled-task action does not match the generated launcher action");
}
if (!homesEqual(registeredClaim.homes, stagedClaim.homes)) {
return unknown("the registered scheduled task names different homes than the staged task definition");
}
Expand Down Expand Up @@ -837,7 +933,7 @@ function homesEqual(
* generated service-asset directory.
*/
function walkWindowsChain(
deps: Required<Pick<ProbeDeps, "home">> & Pick<ProbeDeps, "configDir" | "windowsLocale">,
deps: Required<Pick<ProbeDeps, "home">> & Pick<ProbeDeps, "configDir" | "windowsLocale" | "statePaths">,
xml: string,
definitionPath: string,
): ServiceManagerInstallation {
Expand Down Expand Up @@ -885,9 +981,21 @@ function walkWindowsChain(
return unknown(`the launcher wrapper could not be read: ${String(error)}`);
}

if (!wrapperLooksGenerated(wrapperBody)) {
const launchShape = wrapperLaunchShape(wrapperBody);
if (launchShape === null) {
return unknown(`the launcher wrapper does not look like a generated opencodex service wrapper: ${wrapperPath}`);
}
if (typeof launchShape !== "string") {
const executable = launchShape.standaloneBun;
const evidence = inspectServiceStateEvidence(deps.statePaths ?? serviceStatePathsForOpenCodexHome(configDir));
const valid = evidence.filter(e => e.kind === "valid");
if (!win32Path.isAbsolute(executable) || win32Path.extname(executable).toLowerCase() !== ".exe"
|| valid.length === 0 || evidence.some(e => e.kind === "invalid" || e.kind === "unreadable")
|| valid.some(e => e.state.version !== 2 || e.state.backend !== "scheduler" || e.state.cliPath !== null
|| !e.state.bunPath || normalizeWindowsPath(e.state.bunPath) !== normalizeWindowsPath(executable))) {
Comment on lines +993 to +995

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 Select authoritative state before matching the executable

When a custom OPENCODEX_HOME has a lower-revision compatibility mirror whose refresh previously failed, src/service-manager-probe.ts checks every valid record and rejects the standalone wrapper if that stale mirror retains the prior bunPath, cliPath, or backend. This is a supported state: swapServiceInstallState commits the final authority first and explicitly allows mirror publication to fail, while selectAuthoritativeServiceState treats lower-revision disagreement as repairable. The resulting unknown ownership blocks unattended operations even though the authoritative record and generated wrapper agree; select the authoritative state first and bind the executable to that record, while preserving failure for same/newer unordered conflicts.

Useful? React with 👍 / 👎.

return unknown("the standalone service wrapper executable is not bound to recorded scheduler install state");
}
}

const rawCodexHome = batchSetValue(wrapperBody, "CODEX_HOME");
const rawOpencodexHome = batchSetValue(wrapperBody, "OPENCODEX_HOME");
Expand Down Expand Up @@ -992,6 +1100,7 @@ export function inspectServiceManagerInstallation(deps: ProbeDeps = {}): Service
winswStatus: deps.winswStatus,
windowsLocale: deps.windowsLocale,
windowsTaskListingCache: deps.windowsTaskListingCache,
statePaths: deps.statePaths,
});
}
return unknown(`no service manager probe for platform ${platform}`);
Expand Down
26 changes: 20 additions & 6 deletions src/service/windows-taskxml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,25 @@ function taskXmlDecodedLossyValueEquals(xml: string, tag: string, expected: stri
return taskXmlLossyValueEquals(taskXmlDecodeEntities(value), expected);
}

/** Accept only the single Exec action emitted for this launcher's task. */
export function windowsTaskActionMatches(
xml: string,
launcher: string,
wscript = windowsWscript(),
allowLossyPaths = false,
): boolean {
const scrubbed = taskXmlWithoutCommentsAndCdata(xml);
if (taskXmlElementCount(scrubbed, "Data") > 0 || taskXmlHasPrefixedTag(scrubbed, "Data")
|| taskXmlHasPrefixedTag(scrubbed, "Actions") || taskXmlHasPrefixedTag(scrubbed, "Exec")
|| taskXmlElementCount(scrubbed, "Actions") !== 1 || taskXmlElementCount(scrubbed, "Exec") !== 1) return false;
const actions = taskXmlSection(scrubbed, "Actions");
const exec = /<Exec(?:\s[^>]*)?>([\s\S]*?)<\/Exec>/i.exec(actions);
if (!exec || actions.replace(exec[0], "").trim() !== "") return false;
const equals = allowLossyPaths ? taskXmlDecodedLossyValueEquals : taskXmlDecodedValueEquals;
return equals(exec[1]!, "Command", wscript)
&& equals(exec[1]!, "Arguments", `/b /nologo "${launcher}"`);
}

export function taskXmlOptionalValueEquals(xml: string, tag: string, expected: string): boolean {
// Check the prefixed form first: treating `<t:Enabled>false</t:Enabled>` as an
// omission would turn an explicitly disabled task into a healthy one.
Expand Down Expand Up @@ -520,7 +539,6 @@ function windowsTaskRegistrationBaseHealthy(
const trigger = taskXmlSection(triggers, "LogonTrigger");
const principal = taskXmlSection(scrubbed, "Principal");
const settings = taskXmlSection(scrubbed, "Settings");
const action = taskXmlSection(scrubbed, "Exec");
// A self-closing <LogonTrigger /> leaves an empty section, so look for the element
// itself — scoped to <Triggers> so a decoy elsewhere cannot satisfy it.
return taskXmlElementCount(triggers, "LogonTrigger") > 0
Expand All @@ -538,11 +556,7 @@ function windowsTaskRegistrationBaseHealthy(
// cannot carry when the profile is named outside the code page (#3064). Only
// unrepresentable characters are forgiven; every ASCII segment and every
// separator is still matched literally.
&& (allowLossyPaths
? taskXmlDecodedLossyValueEquals(action, "Command", wscript)
&& taskXmlDecodedLossyValueEquals(action, "Arguments", `/b /nologo "${launcher}"`)
: taskXmlDecodedValueEquals(action, "Command", wscript)
&& taskXmlDecodedValueEquals(action, "Arguments", `/b /nologo "${launcher}"`));
&& windowsTaskActionMatches(xml, launcher, wscript, allowLossyPaths);
}

/** Validate the security/lifecycle-critical fields of the registered scheduler task. */
Expand Down
14 changes: 14 additions & 0 deletions structure/ops/service-and-sidecars.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,20 @@ two naming different homes, and on macOS a logged-out user can have the plist on
domain to query. The probe returns what it saw and does not decide ownership; callers such as
`src/integrations/native/ownership-preflight.ts` compare the homes. Every command it runs is
read-only and time-bounded, so it is safe while the proxy runs under that same manager.
On Windows, the generated-wrapper check accepts package installs that invoke the source CLI.
A standalone wrapper that invokes `start` directly must carry the generated protocol and runtime
markers, one quoted `OCX_BUN` assignment, and no `OCX_CLI` assignment in either quoting form.
Its executable lines and control-flow order must match the standalone script emitted by
`src/service/windows-taskxml.ts`; added jumps, exits, calls, labels, or commands make the probe unknown.
When Task Scheduler reports a registered task, the probe also requires its action to contain exactly
one Exec with the generated `wscript.exe` command and exact `/b /nologo` launcher arguments.
A foreign command or additional action makes ownership unknown even if the wrapper and homes agree.
Its executable must be absolute, end in `.exe`, and agree with `bunPath` in every readable service
state record for the scheduler backend with `cliPath: null`. Missing, malformed, or contradictory
state leaves the probe unknown; it cannot authorize unattended native Codex writes.
The state records a lexical executable path, not an install-time file identity or digest. A
retargeted junction or replacement at the same path is therefore outside this probe's evidence;
resolving the path only at probe time cannot establish which file the installer recorded.

## Stable service launcher (launchd and systemd)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "../../src/service-manager-probe";
import { inspectNativeCodexOwnership } from "../../src/integrations/native/ownership-preflight";
import { setTrustedWindowsSystemDirectoryResolverForTests } from "../../src/lib/windows-elevation";
import { windowsWscript } from "../../src/service/windows-scheduler";
import { getDefaultConfig } from "../../src/config";
import { startServer } from "../../src/server";
import { removeTreeWithRetry } from "../helpers/remove-tree";
Expand Down Expand Up @@ -74,6 +75,7 @@ function schedulerXml(launcherPath: string): string {
"<Task>",
" <Actions>",
" <Exec>",
` <Command>${windowsWscript()}</Command>`,
` <Arguments>/b /nologo &quot;${escaped}&quot;</Arguments>`,
" </Exec>",
" </Actions>",
Expand All @@ -99,7 +101,7 @@ function writeSchedulerChain(
'set "OCX_BUN=C:\\bun\\bun.exe"',
'set "OCX_CLI=C:\\opencodex\\src\\cli\\index.ts"',
":loop",
'"%OCX_BUN%" "%OCX_CLI%" start --port 10100',
'"%OCX_BUN%" "%OCX_CLI%" start --port 10100 >>"%OCX_SERVICE_LOG%" 2>&1',
].join("\r\n"));
writeFileSync(launcher, `shell.Run """${wrapper}""", 0, True\r\n`);
if (options.writeTaskXml !== false) writeFileSync(taskXml, schedulerXml(launcher));
Expand Down
Loading
Loading