Skip to content
Open
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
16 changes: 11 additions & 5 deletions bin/ocx.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ function bunBinDir() {
}

const BUN_OVERRIDE_ENV = "OPENCODEX_BUN_PATH";
const BUN_RUNTIME_SOURCE_ENV = "OCX_BUN_RUNTIME_SOURCE";

function findBunBinary(bunDir) {
// The npm `bun` package ships the binary as bin/bun.exe on every platform;
Expand Down Expand Up @@ -319,7 +320,7 @@ function resolveBun() {
const override = process.env[BUN_OVERRIDE_ENV]?.trim();
if (override) {
const overridePath = resolve(override);
if (isRealBunBinary(overridePath)) return overridePath;
if (isRealBunBinary(overridePath)) return { path: overridePath, source: "override" };
console.error(
`opencodex: ${BUN_OVERRIDE_ENV} is missing, unreadable, or not a complete Bun binary; falling back to the bundled runtime.`,
);
Expand All @@ -333,7 +334,7 @@ function resolveBun() {
}

let bin = findBunBinary(bunDir);
if (bin) return bin;
if (bin) return { path: bin, source: "bundled" };

// Lazy fallback: --ignore-scripts (or a failed postinstall) leaves the
// ~450-byte placeholder stub. Run the bun package's own installer once.
Expand All @@ -343,7 +344,7 @@ function resolveBun() {
if (r.status === 0) bin = findBunBinary(bunDir);
}
if (!bin) fail("Bun binary missing after install attempt.");
return bin;
return { path: bin, source: "bundled" };
}

// `ocx update --help` prints usage and exits WITHOUT side effects. The npm launcher
Expand All @@ -361,7 +362,8 @@ if (process.argv[2] === "update" && isNodeModulesInstall() && !isBunGlobalInstal
runNpmSelfUpdate();
}

const bun = resolveBun();
const runtime = resolveBun();
const bun = runtime.path;

// Run the Bun child asynchronously and FORWARD termination signals to it, then wait
// for its graceful shutdown before this launcher exits. The previous blocking
Expand All @@ -386,7 +388,11 @@ const preBunAnthropicSlots = ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"]
.filter(name => typeof process.env[name] === "string" && process.env[name] !== "");
const child = spawn(bun, [cliPath, ...process.argv.slice(2)], {
stdio: "inherit",
env: { ...process.env, OCX_PRE_BUN_ANTHROPIC_ENV: preBunAnthropicSlots.join(",") },
env: {
...process.env,
OCX_PRE_BUN_ANTHROPIC_ENV: preBunAnthropicSlots.join(","),
[BUN_RUNTIME_SOURCE_ENV]: runtime.source,
},
});

// Windows has no real POSIX signals (no SIGHUP); forwarding is best-effort there.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,13 @@ runtime the leak itself remains an upstream problem:
Windows working-set/RSS counters can under-report committed external
retention.
- **`ocx doctor`** — a "Memory / runtime" section shows the *service*
process's Bun version, RSS, external/ArrayBuffers counters, JS-heap context,
and stream-mode decision. On the bundled Bun 1.3.14 runtime, `heapUsed` /
process's Bun version/revision, launcher-reported runtime source, RSS,
external/ArrayBuffers counters, JS-heap context, and stream-mode decision.
Source is allowlisted as `override`, `bundled`, or `process`; an older service
or invalid/missing marker is shown as unknown instead of being guessed from
the current shell. An active override remains unvalidated and keeps the
conservative runtime gate, but doctor no longer repeats the already-completed
`OPENCODEX_BUN_PATH` setup step. On the bundled Bun 1.3.14 runtime, `heapUsed` /
`jscHeap` alone are not a leak discriminator; compare observed memory with
`responseState` and repeated samples before assigning an app-level leak.
- **`GET /api/system/memory`** — the same data over the authenticated
Expand Down
32 changes: 29 additions & 3 deletions src/cli/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { gracefulStopHost } from "../lib/process-control";
import { maskAccountId } from "../lib/privacy";
import { PROXY_ENV_KEYS, proxyEnvPresent } from "../lib/proxy-env";
import { configuredAdminToken } from "../lib/admin-secrets";
import type { BunRuntimeSource } from "../lib/bun-runtime";
import { readCodexTokens } from "../codex/auth-collision";
import { collectOrcaCodexHomeDiagnostic, resolveCodexHomeDir as resolveCodexHomeDirImpl, isWslRuntime, listWslWindowsCodexHomes, wslAutomountRoot, type CodexHomeDeps } from "../codex/home";
import { findCodexOnPath, isWindowsInteropDir } from "../codex/shim";
Expand Down Expand Up @@ -523,6 +524,8 @@ export async function probeWham(fetchImpl: typeof fetch = fetch): Promise<WhamPr
export type ServiceMemoryData = {
pid: number;
bunVersion: string;
bunRevision?: string;
bunRuntimeSource?: BunRuntimeSource;
platform: string;
rss: number;
heapUsed: number;
Expand Down Expand Up @@ -579,6 +582,12 @@ export async function fetchServiceMemory(
data: {
pid: body.pid,
bunVersion: body.bunVersion,
bunRevision: typeof body.bunRevision === "string" ? body.bunRevision : undefined,
bunRuntimeSource: body.bunRuntimeSource === "override"
|| body.bunRuntimeSource === "bundled"
|| body.bunRuntimeSource === "process"
? body.bunRuntimeSource
: undefined,
platform: typeof body.platform === "string" ? body.platform : "unknown",
rss: body.rss,
heapUsed: typeof body.heapUsed === "number" ? body.heapUsed : 0,
Expand Down Expand Up @@ -625,7 +634,13 @@ export function formatServiceMemoryLines(report: ServiceMemoryReport): string[]
return lines;
}
const d = report.data;
lines.push(` ok service pid ${d.pid}: Bun ${d.bunVersion} on ${d.platform}`);
const sourceLabel = d.bunRuntimeSource === "override"
? "OPENCODEX_BUN_PATH override"
: d.bunRuntimeSource ?? "unknown";
lines.push(
` ok service pid ${d.pid}: Bun ${d.bunVersion} on ${d.platform}`
+ `${d.bunRevision ? ` (revision=${d.bunRevision}, source=${sourceLabel})` : ` (source=${sourceLabel})`}`,
);
const observed = observedMemory(d);
const observedBytes = d.observedBytes ?? d.watchdog?.observedBytes ?? observed.bytes;
const observedMetric = d.observedMetric ?? d.watchdog?.observedMetric ?? observed.metric;
Expand All @@ -652,9 +667,20 @@ export function formatServiceMemoryLines(report: ServiceMemoryReport): string[]
} else {
lines.push(" !! high RSS, indeterminate split — capture two doctor runs over time to see the trend");
}
// Version-claiming (never binary-claiming): the endpoint cannot distinguish
// the bundled binary from an OPENCODEX_BUN_PATH override of the same version.
// Runtime provenance is launcher-asserted and allowlisted. A missing marker is
// intentionally unknown; never infer an already-running service from this shell.
if (d.platform === "win32" && d.eagerRelay?.reason === "auto-known-bad") {
if (d.bunRuntimeSource === "override") {
lines.push(" OPENCODEX_BUN_PATH is already active, but this runtime remains unvalidated for automatic eager relay.");
lines.push(" The conservative auto-known-bad decision remains in effect; bunRevision is informational only.");
lines.push(" You can still opt into streamMode \"eager-relay\" via PUT /api/settings (crash risk on this runtime; see docs).");
return lines;
Comment thread
luvs01 marked this conversation as resolved.
}
if (d.bunRuntimeSource === undefined) {
lines.push(" runtime source was not reported by this legacy service/payload; an active override cannot be determined safely.");
lines.push(" Repair or reinstall the service to add provenance; the conservative auto-known-bad decision remains in effect.");
return lines;
}
lines.push(` service is running Bun ${d.bunVersion} on Windows — a version affected by the upstream Bun memory issue.`);
lines.push(" Options: wait for a bundled runtime update, or set OPENCODEX_BUN_PATH to a runtime you trust (unvalidated — own risk),");
lines.push(" or opt into streamMode \"eager-relay\" via PUT /api/settings (crash risk on this runtime; see docs).");
Expand Down
61 changes: 48 additions & 13 deletions src/codex/shim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
writeFileSync,
} from "node:fs";
import { getConfigDir } from "../config";
import { durableBunPath } from "../lib/bun-runtime";
import { BUN_RUNTIME_SOURCE_ENV, durableBunRuntime, type BunRuntimeSource } from "../lib/bun-runtime";
import { isProcessAlive } from "../lib/process-control";
import { serviceApiTokenFilePath } from "../lib/service-secrets";
import { recordOwnedConfigPath } from "../lib/config-ownership";
Expand Down Expand Up @@ -120,11 +120,16 @@ export type CodexShimAutoRestoreResult =
| { status: "ineligible" | "deferred"; message?: string }
| { status: "restored"; message: string };

function cliEntry(): { bun: string; cli: string } {
// Bundled Bun path (survives `ocx update`); all three shim builders
// (Unix / Windows cmd / Windows PowerShell) receive it via this entry.
function cliEntry(): { bun: string; bunRuntimeSource: BunRuntimeSource; cli: string } {
// Resolve the durable path and its diagnostic provenance together; all three shim
// builders (Unix / Windows cmd / Windows PowerShell) receive the same pair.
// This module lives in src/codex/, the CLI entry in src/cli/index.ts.
return { bun: durableBunPath(), cli: join(import.meta.dir, "..", "cli", "index.ts") };
const bunRuntime = durableBunRuntime();
return {
bun: bunRuntime.path,
bunRuntimeSource: bunRuntime.source,
cli: join(import.meta.dir, "..", "cli", "index.ts"),
};
}

function commandNames(name: string): string[] {
Expand Down Expand Up @@ -366,11 +371,19 @@ function shQuote(value: string): string {
return `'${value.replace(/'/g, "'\\''")}'`;
}

export function buildUnixCodexShim(realCodexPath: string, bunPath: string, cliPath: string, tokenFile = serviceApiTokenFilePath()): string {
export function buildUnixCodexShim(
realCodexPath: string,
bunPath: string,
cliPath: string,
tokenFile = serviceApiTokenFilePath(),
bunRuntimeSource: BunRuntimeSource = "process",
): string {
const internalCommands = CODEX_INTERNAL_COMMANDS.join("|");
const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.join("|");
return `#!/usr/bin/env sh
# ${SHIM_MARKER}
${BUN_RUNTIME_SOURCE_ENV}=${shQuote(bunRuntimeSource)}
export ${BUN_RUNTIME_SOURCE_ENV}
if [ -z "$OPENCODEX_API_AUTH_TOKEN" ] && [ -f ${shQuote(tokenFile)} ]; then
OPENCODEX_API_AUTH_TOKEN="$(cat ${shQuote(tokenFile)})"
export OPENCODEX_API_AUTH_TOKEN
Expand Down Expand Up @@ -431,14 +444,20 @@ function windowsBatchSet(name: string, value: string): string {
return `set "${name}=${windowsEnvIndirectBatchValue(value, windowsBatchValue)}"`;
}

export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
export function buildWindowsCodexShim(
realCodexPath: string,
bunPath: string,
cliPath: string,
bunRuntimeSource: BunRuntimeSource = "process",
): string {
const internalCommandChecks = CODEX_INTERNAL_COMMANDS.map(command => `if /I "%~1"=="${command}" goto run_codex`).join("\r\n");
const valueOptionChecks = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => `if /I "%~1"=="${option}" goto skip_option_value`).join("\r\n");
return `@echo off\r
rem ${SHIM_MARKER}\r
${windowsBatchSet("OCX_REAL_CODEX", realCodexPath)}\r
${windowsBatchSet("OCX_BUN", bunPath)}\r
${windowsBatchSet("OCX_CLI", cliPath)}\r
${windowsBatchSet(BUN_RUNTIME_SOURCE_ENV, bunRuntimeSource)}\r
${windowsBatchSet("OCX_API_TOKEN_FILE", serviceApiTokenFilePath())}\r
if "%OPENCODEX_API_AUTH_TOKEN%"=="" if exist "%OCX_API_TOKEN_FILE%" set /p OPENCODEX_API_AUTH_TOKEN=<"%OCX_API_TOKEN_FILE%"\r
if not "%OCX_SHIM_BYPASS%"=="" goto run_codex\r
Expand Down Expand Up @@ -472,12 +491,18 @@ function psString(value: string): string {
return `'${value.replace(/'/g, "''")}'`;
}

export function buildWindowsPowerShellCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
export function buildWindowsPowerShellCodexShim(
realCodexPath: string,
bunPath: string,
cliPath: string,
bunRuntimeSource: BunRuntimeSource = "process",
): string {
const internalCommands = CODEX_INTERNAL_COMMANDS.map(command => psString(command)).join(", ");
const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => psString(option)).join(", ");
const tokenFile = serviceApiTokenFilePath();
return `#!/usr/bin/env pwsh
# ${SHIM_MARKER}
$env:${BUN_RUNTIME_SOURCE_ENV} = ${psString(bunRuntimeSource)}
if (-not $env:OPENCODEX_API_AUTH_TOKEN -and (Test-Path -LiteralPath ${psString(tokenFile)})) {
$env:OPENCODEX_API_AUTH_TOKEN = (Get-Content -Raw -LiteralPath ${psString(tokenFile)}).Trim()
}
Expand Down Expand Up @@ -601,25 +626,35 @@ function gitBashPath(path: string): string {
}

function writeShim(wrapperPath: string, realCodexPath: string): void {
const { bun, cli } = cliEntry();
const { bun, bunRuntimeSource, cli } = cliEntry();
if (process.platform === "win32") {
const lower = wrapperPath.toLowerCase();
if (lower.endsWith(".ps1")) {
// UTF-8 BOM: Windows PowerShell 5.1 decodes BOM-less .ps1 files in the ANSI
// codepage, which mangles non-ASCII paths embedded in the shim.
writeFileSync(wrapperPath, `\uFEFF${buildWindowsPowerShellCodexShim(realCodexPath, bun, cli)}`, "utf8");
writeFileSync(wrapperPath, `\uFEFF${buildWindowsPowerShellCodexShim(realCodexPath, bun, cli, bunRuntimeSource)}`, "utf8");
} else if (lower.endsWith(".cmd") || lower.endsWith(".bat")) {
writeFileSync(wrapperPath, buildWindowsCodexShim(realCodexPath, bun, cli), "utf8");
writeFileSync(wrapperPath, buildWindowsCodexShim(realCodexPath, bun, cli, bunRuntimeSource), "utf8");
} else {
// Extensionless Git-Bash sh launcher: sh shim with forward-slash paths.
writeFileSync(
wrapperPath,
buildUnixCodexShim(gitBashPath(realCodexPath), gitBashPath(bun), gitBashPath(cli), gitBashPath(serviceApiTokenFilePath())),
buildUnixCodexShim(
gitBashPath(realCodexPath),
gitBashPath(bun),
gitBashPath(cli),
gitBashPath(serviceApiTokenFilePath()),
bunRuntimeSource,
),
"utf8",
);
}
} else {
writeFileSync(wrapperPath, buildUnixCodexShim(realCodexPath, bun, cli), "utf8");
writeFileSync(
wrapperPath,
buildUnixCodexShim(realCodexPath, bun, cli, serviceApiTokenFilePath(), bunRuntimeSource),
"utf8",
);
chmodSync(wrapperPath, 0o755);
}
}
Expand Down
21 changes: 20 additions & 1 deletion src/lib/bun-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@ export { isRealBunBinary };
const require = createRequire(import.meta.url);

const BUN_OVERRIDE_ENV = "OPENCODEX_BUN_PATH";
export const BUN_RUNTIME_SOURCE_ENV = "OCX_BUN_RUNTIME_SOURCE";

export type BunRuntimeSource = "override" | "bundled" | "process";

export type DurableBunRuntime = {
path: string;
source: "override" | "bundled" | "process";
source: BunRuntimeSource;
overrideEnv: typeof BUN_OVERRIDE_ENV;
};

Expand Down Expand Up @@ -60,6 +63,22 @@ export function durableBunRuntime(): DurableBunRuntime {
return { path: process.execPath, source: "process", overrideEnv: BUN_OVERRIDE_ENV };
}

/**
* Runtime origin asserted by the launcher that started this process.
*
* This is diagnostic provenance, not a security boundary. Accept only the
* launcher allowlist and leave a missing/invalid marker unknown. In particular,
* never inspect the current shell to guess how an already-running service began.
*/
export function reportedBunRuntimeSource(
env: NodeJS.ProcessEnv = process.env,
): BunRuntimeSource | undefined {
const source = env[BUN_RUNTIME_SOURCE_ENV]?.trim();
return source === "override" || source === "bundled" || source === "process"
? source
: undefined;
Comment thread
luvs01 marked this conversation as resolved.
}

/**
* Bun path to bake into durable artifacts (launchd/systemd/Task Scheduler and
* the Codex auto-start shim). Prefer the bundled binary — it lives under the
Expand Down
11 changes: 9 additions & 2 deletions src/lib/winsw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { homedir } from "node:os";
import { join, resolve } from "node:path";
import { expandUserPath, getConfigDir, loadConfig } from "../config";
import { recordOwnedConfigPath } from "./config-ownership";
import { durableBunPath } from "./bun-runtime";
import { BUN_RUNTIME_SOURCE_ENV, durableBunRuntime, type BunRuntimeSource } from "./bun-runtime";
import { serviceApiTokenFilePath } from "./service-secrets";

export const WINSW_VERSION = "2.12.0";
Expand Down Expand Up @@ -64,6 +64,7 @@ function currentCodexHomeAbsolute(): string {

export interface WinswEntry {
bun: string;
bunRuntimeSource: BunRuntimeSource;
cli: string;
}

Expand Down Expand Up @@ -95,6 +96,7 @@ export function buildWinswXml(entry: WinswEntry, env: NodeJS.ProcessEnv = proces
const aclTimeout = env.OPENCODEX_ACL_TIMEOUT_MS?.trim();
const envLines = [
` <env name="OCX_SERVICE" value="1"/>`,
` <env name="${BUN_RUNTIME_SOURCE_ENV}" value="${entry.bunRuntimeSource}"/>`,
` <env name="OCX_API_TOKEN_FILE" value="${xmlEscape(serviceApiTokenFilePath())}"/>`,
` <env name="PATH" value="${xmlEscape(env.PATH ?? "")}"/>`,
env.CODEX_HOME?.trim() ? ` <env name="CODEX_HOME" value="${xmlEscape(currentCodexHomeAbsolute())}"/>` : null,
Expand Down Expand Up @@ -371,5 +373,10 @@ export function winswStatusSummary(): string {

/** Default entry mirrors the Task Scheduler baking: durable Bun + cli.ts. */
export function defaultWinswEntry(cliDir: string): WinswEntry {
return { bun: durableBunPath(), cli: join(cliDir, "cli", "index.ts") };
const bunRuntime = durableBunRuntime();
return {
bun: bunRuntime.path,
bunRuntimeSource: bunRuntime.source,
cli: join(cliDir, "cli", "index.ts"),
};
}
2 changes: 2 additions & 0 deletions src/server/management/system-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
* dashboard drain-and-restart confirm UX — never request bodies or IDs.
*/
import { selectEagerPath } from "../../lib/bun-stream-caps";
import { reportedBunRuntimeSource } from "../../lib/bun-runtime";
import { getActiveTurnCount, isDraining } from "../lifecycle";
import { getActiveMemoryWatchdog, observedMemoryCounter } from "../memory-watchdog";
import { responseStateMetrics } from "../../responses/state";
Expand Down Expand Up @@ -78,6 +79,7 @@ export async function handleSystemRoutes(ctx: ManagementContext): Promise<Respon
pid: process.pid,
bunVersion: Bun.version,
bunRevision: Bun.revision,
bunRuntimeSource: reportedBunRuntimeSource(),
platform: process.platform,
uptimeSeconds: process.uptime(),
rss: usage.rss,
Expand Down
Loading
Loading