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
7 changes: 5 additions & 2 deletions skills/ocx/references/01_surface_integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -855,7 +855,9 @@ Drives no management route.
JSON mode: `none`.

- Local launcher/PATH wrapper operation on macOS, Linux and Windows; no management API or JSON mode.
- Installation diagnoses wrapper health and reports readiness warnings.
- On macOS/Linux, installs the private PATH overlay and explicitly migrates legacy in-place shims; automatic repair does not migrate them.
- On macOS/Linux with sh/bash/zsh, source the printed codex-shell-env.sh path, then add that line after PATH setup in your shell startup file.
- Installation diagnoses wrapper health and reports readiness warnings; Windows keeps in-place wrappers.

### `ocx codex-shim status`

Expand All @@ -870,7 +872,8 @@ Drives no management route.
JSON mode: `none`.

- Local launcher/PATH wrapper operation on macOS, Linux and Windows; no management API or JSON mode.
- Inspects local launcher wrapper state.
- Reports wrapper health, Unix overlay PATH activation, and explicit legacy migration guidance.
- For JSON, run ocx status --json and inspect codexShim; this command rejects --json and unexpected arguments.

### `ocx codex-shim uninstall`

Expand Down
4 changes: 2 additions & 2 deletions src/cli/capabilities-observe-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -581,15 +581,15 @@ export const OBSERVE_SYSTEM_CAPABILITIES: readonly Capability[] = [
routes: [],
flags: [],
mutates: true, json: "none",
details: ["Local launcher/PATH wrapper operation on macOS, Linux and Windows; no management API or JSON mode.", "Installation diagnoses wrapper health and reports readiness warnings."],
details: ["Local launcher/PATH wrapper operation on macOS, Linux and Windows; no management API or JSON mode.", "On macOS/Linux, installs the private PATH overlay and explicitly migrates legacy in-place shims; automatic repair does not migrate them.", "On macOS/Linux with sh/bash/zsh, source the printed codex-shell-env.sh path, then add that line after PATH setup in your shell startup file.", "Installation diagnoses wrapper health and reports readiness warnings; Windows keeps in-place wrappers."],
},
{
command: ["codex-shim", "status"], summary: "Local Codex launcher shim status.",
usage: "ocx codex-shim status",
routes: [],
flags: [],
mutates: false, json: "none",
details: ["Local launcher/PATH wrapper operation on macOS, Linux and Windows; no management API or JSON mode.", "Inspects local launcher wrapper state."],
details: ["Local launcher/PATH wrapper operation on macOS, Linux and Windows; no management API or JSON mode.", "Reports wrapper health, Unix overlay PATH activation, and explicit legacy migration guidance.", "For JSON, run ocx status --json and inspect codexShim; this command rejects --json and unexpected arguments."],
},
{
command: ["codex-shim", "uninstall"], summary: "Local Codex launcher shim uninstall.",
Expand Down
11 changes: 10 additions & 1 deletion src/cli/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -662,9 +662,18 @@ const commandRunners: Record<string, CommandRunner> = {
if (!success) console.error(`${r.refused ? "Codex shim installation was refused" : "Codex shim installation is unhealthy"}: ${summary}`);
return success ? 0 : 1;
}
case "status":
case "status": {
const extra = deps.args.slice(2);
if (extra.length > 0) {
console.error(extra.some(isJsonOption)
? "ocx codex-shim status does not support --json; use ocx status --json (codexShim)."
: "ocx codex-shim status does not accept arguments or options.");
console.error("Usage: ocx codex-shim status");
return 2;
}
console.log(codexShimStatus());
break;
}
case "uninstall":
case "remove": {
const r = uninstallCodexShim();
Expand Down
19 changes: 17 additions & 2 deletions src/cli/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,12 +487,21 @@ export type CodexEnvKeyReadinessDiagnostic = {
action: string;
};

/** Shell activation and legacy migration are independent of admission-token readiness. */
export function formatCodexShimDoctorLines(shim: CodexShimDiagnostic, platform = process.platform): string[] {
if (platform === "win32" || !shim.installed) return [];
const inactiveOverlay = shim.runnable === true && shim.active === false;

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 Show guidance when overlay activation is unverified

When the overlay is runnable but PATH inspection cannot fingerprint the selected command—for example, because the entry changes during inspection—overlayDiagnostic returns active: null and a summary explicitly reporting PATH unverified with the activation hint. Requiring active === false drops that summary from ocx doctor, leaving this uncertain launch state unexplained. Treat any runnable overlay whose activation is not confirmed (active !== true) as needing the diagnostic line.

Useful? React with 👍 / 👎.

const healthyLegacy = shim.healthy && shim.runnable === undefined;
return inactiveOverlay || healthyLegacy ? shim.summary.split("\n").map(line => ` ${line}`) : [];
}

/** Warn when routed Codex cannot obtain its configured admission token at launch. */
export function collectCodexEnvKeyReadiness(
configText: string | null,
env: EnvMap,
shim: CodexShimDiagnostic,
serviceTokenPresent: boolean,
shimActivationShown = false,
): CodexEnvKeyReadinessDiagnostic | null {
if (!configText || rootTomlString(configText, "model_provider") !== "opencodex") return null;
const envName = providerTableString(configText, "opencodex", "env_key")?.trim();
Expand All @@ -504,7 +513,9 @@ export function collectCodexEnvKeyReadiness(
shimState,
detail: `Codex uses env_key ${envName}, but that variable is unset and the OpenCodex shim is ${shimState}; the service token file exists but plain Codex does not load it`,
action: shimState === "inactive"
? `${overlayActivationHint()} Or export ${envName} in the process that starts Codex`
? shimActivationShown
? `Activate the PATH shim as shown under Codex restart safety, or export ${envName} in the process that starts Codex`
: `${overlayActivationHint()} Or export ${envName} in the process that starts Codex`
: `Run 'ocx codex-shim install' to repair launch-time token injection, or export ${envName} in the process that starts Codex`,
};
}
Expand Down Expand Up @@ -1352,11 +1363,14 @@ export async function runDoctor(args: string[] = []): Promise<void> {
try { return readFileSync(codexConfigPath, "utf8"); } catch { return null; }
})();
const serviceTokenPresent = Boolean(readInstalledServiceToken()?.trim());
const codexShim = diagnoseCodexShim();
const codexShimLines = formatCodexShimDoctorLines(codexShim);
const codexEnvKeyReadiness = collectCodexEnvKeyReadiness(
codexConfigText,
process.env,
diagnoseCodexShim(),
codexShim,
serviceTokenPresent,
codexShimLines.length > 0,
);
// Use the same attested live startup verdict as `ocx status` when the proxy is already
// identity-verified. A shell-local systemd probe can be a false negative for a system-wide
Expand All @@ -1369,6 +1383,7 @@ export async function runDoctor(args: string[] = []): Promise<void> {
console.log("\nCodex restart safety");
console.log(` ${startup.rebootSafe ? "ok " : "!! "} ${startupHealthSummary(startup)}`);
console.log(` ${formatStartupRoutingDetail(startup)}`);
for (const line of codexShimLines) console.log(line);

console.log("\nCodex runtime selection");
{
Expand Down
5 changes: 4 additions & 1 deletion src/codex/shim-diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ export function diagnoseCodexShim(): CodexShimDiagnostic {
return `Codex autostart shim: wrapper ${wrapper} at ${file.wrapperPath}; original backup ${backup} at ${file.backupPath}.`;
}).join("\n");
// Presence alone is not health: a damaged wrapper still reads "shim present", so state the verdict.
return { installed: true, healthy, summary: healthy ? summary
const migration = healthy && state.platform !== "win32"
? " Legacy Unix shim installed in place; automatic repair does not migrate it. Run ocx codex-shim install, then source the printed codex-shell-env.sh path and add that line after PATH setup in your shell startup file."
: "";
return { installed: true, healthy, summary: healthy ? `${summary}${migration}`
: `${summary}\nCodex autostart shim is unhealthy. Run ocx codex-shim install to repair it.` };
}

Expand Down
44 changes: 44 additions & 0 deletions tests/cli/cli-dispatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1060,6 +1060,50 @@ describe("doctor refuses --json rather than printing prose as success", () => {
});
});

describe("codex-shim status argument validation", () => {
test.each(["--json", "--json=true", "--nope", "unexpected"])("rejects %s with stderr-only usage and exit 2", async extra => {
const out: string[] = [];
const err: string[] = [];
const logSpy = spyOn(console, "log").mockImplementation(value => { out.push(String(value)); });
const errorSpy = spyOn(console, "error").mockImplementation(value => { err.push(String(value)); });
try {
const args = ["codex-shim", "status", extra];
expect(await dispatchCommand(
{ kind: "command", command: "codex-shim", args }, { ...fakeDeps, args },
)).toBe(2);
expect(out).toEqual([]);
expect(err).toContain("Usage: ocx codex-shim status");
expect(err[0]).toBe(extra.startsWith("--json")
? "ocx codex-shim status does not support --json; use ocx status --json (codexShim)."
: "ocx codex-shim status does not accept arguments or options.");
expect(err.join("\n")).not.toContain(extra === "unexpected" ? extra : "Codex autostart shim:");
} finally {
logSpy.mockRestore();
errorSpy.mockRestore();
}
});

test("bare status still prints the local diagnosis and exits 0", async () => {
const home = mkdtempSync(join(tmpdir(), "ocx-shim-status-"));
const previous = process.env.OPENCODEX_HOME;
const out: string[] = [];
const logSpy = spyOn(console, "log").mockImplementation(value => { out.push(String(value)); });
try {
process.env.OPENCODEX_HOME = home;
const args = ["codex-shim", "status"];
expect(await dispatchCommand(
{ kind: "command", command: "codex-shim", args }, { ...fakeDeps, args },
)).toBe(0);
expect(out).toEqual(["Codex autostart shim is not installed."]);
} finally {
logSpy.mockRestore();
if (previous === undefined) delete process.env.OPENCODEX_HOME;
else process.env.OPENCODEX_HOME = previous;
removeTreeWithRetry(home);
}
});
});

describe("GUI command delegation", () => {
const config = {
port: 10100,
Expand Down
24 changes: 23 additions & 1 deletion tests/codex-integration/codex-shim-migration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, expect, mock, spyOn, test } from "bun:test";
import * as fs from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { autoRestoreCodexShim, buildUnixCodexShim, installCodexShim, setCodexShimProbeHookForTests, setCodexShimProbeObservationMsForTests, uninstallCodexShim } from "../../src/codex/shim";
import { autoRestoreCodexShim, buildUnixCodexShim, codexShimStatus, diagnoseCodexShim, installCodexShim, setCodexShimProbeHookForTests, setCodexShimProbeObservationMsForTests, uninstallCodexShim } from "../../src/codex/shim";
import { readState, type ShimState } from "../../src/codex/shim-state-file";

function fixture(run: (f: { root: string; home: string; native: string; backup: string; quarantine: string; wrapper: string; statePath: string; state: ShimState; legacyBytes: string; version: (name: string) => string }) => void): void {
Expand Down Expand Up @@ -46,6 +46,27 @@ function fixture(run: (f: { root: string; home: string; native: string; backup:
const lexical = (path: string) => { try { return fs.lstatSync(path); } catch { return null; } };

describe.skipIf(process.platform === "win32")("legacy Unix shim migration", () => {
test("healthy legacy status advises explicit migration without changing health or files", () => fixture(f => {
const raw = fs.readFileSync(f.statePath, "utf8");
const diagnostic = diagnoseCodexShim();
expect(diagnostic).toMatchObject({ installed: true, healthy: true });
expect(codexShimStatus()).toBe(diagnostic.summary);
expect(diagnostic.summary).toContain("Legacy Unix shim installed in place; automatic repair does not migrate it.");
expect(diagnostic.summary).toContain("Run ocx codex-shim install, then source the printed codex-shell-env.sh path");
expect(diagnostic.summary).toContain("after PATH setup in your shell startup file");
expect(fs.readFileSync(f.statePath, "utf8")).toBe(raw);
expect(fs.readFileSync(f.native, "utf8")).toBe(f.legacyBytes);
expect(lexical(join(f.home, "codex-shell-env.sh"))).toBeNull();
}));

test("Windows legacy diagnostics omit Unix migration and sourcing advice", () => fixture(f => {
fs.writeFileSync(f.statePath, JSON.stringify({ ...f.state, platform: "win32" }));
const diagnostic = diagnoseCodexShim();
expect(diagnostic).toMatchObject({ installed: true, healthy: true });
expect(diagnostic.summary).not.toContain("Legacy Unix");
expect(diagnostic.summary).not.toContain("codex-shell-env.sh");
}));

test("explicit install restores backup symlink and commits schema 2 without in-place wrapping", () => fixture(f => {
const target = fs.readlinkSync(f.backup);
expect(autoRestoreCodexShim({ enabled: () => true }).status).toBe("ineligible");
Expand All @@ -55,6 +76,7 @@ describe.skipIf(process.platform === "win32")("legacy Unix shim migration", () =
expect(lexical(f.backup)).toBeNull();
expect(lexical(f.quarantine)).toBeNull();
expect(readState()).toMatchObject({ schema: 2, launcherPath: f.native });
expect(diagnoseCodexShim().summary).not.toContain("Legacy Unix");
expect(installCodexShim()).toMatchObject({ installed: false, runnable: true });
}));

Expand Down
48 changes: 45 additions & 3 deletions tests/service/doctor-codex-envkey-readiness.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { isolateCodexShimEnvironment } from "../helpers/codex-shim-install-fixture";
import { isolateCodexShimEnvironment, withInstalledShim } from "../helpers/codex-shim-install-fixture";
import { describe, expect, test } from "bun:test";
import { collectCodexEnvKeyReadiness } from "../../src/cli/doctor";
import type { CodexShimDiagnostic } from "../../src/codex/shim";
import { collectCodexEnvKeyReadiness, formatCodexShimDoctorLines } from "../../src/cli/doctor";
import { diagnoseCodexShim, type CodexShimDiagnostic } from "../../src/codex/shim";
import { join } from "node:path";

isolateCodexShimEnvironment();

Expand Down Expand Up @@ -81,3 +82,44 @@ describe("doctor Codex env_key launch readiness", () => {
expect(JSON.stringify(row)).not.toContain(token);
});
});

describe.skipIf(process.platform === "win32")("doctor general Codex shim guidance", () => {
test.each([
{ name: "no Codex config", text: null, env: {}, token: true },
{ name: "native provider", text: 'model_provider = "openai"', env: {}, token: true },
{ name: "exported env_key", text: config, env: { OPENCODEX_API_AUTH_TOKEN: "set" }, token: true },
{ name: "no service token", text: config, env: {}, token: false },
])("inactive overlay prints shell activation with $name", scenario => withInstalledShim(f => {
const shim = diagnoseCodexShim();
expect(shim).toMatchObject({ runnable: true, active: false });
expect(collectCodexEnvKeyReadiness(scenario.text, scenario.env, shim, scenario.token)).toBeNull();
const lines = formatCodexShimDoctorLines(shim).join("\n");
expect(lines).toContain(`run . '${join(f.home, "codex-shell-env.sh")}'`);
expect(lines).toContain("after other PATH setup in your shell startup file");
}));

test("token warning refers to restart guidance without duplicating the source command", () => withInstalledShim(() => {
const shim = diagnoseCodexShim();
const lines = formatCodexShimDoctorLines(shim);
const row = collectCodexEnvKeyReadiness(config, {}, shim, true, lines.length > 0);
expect(row?.shimState).toBe("inactive");
expect(row?.detail).toContain("variable is unset");
expect(row?.action).toContain("Codex restart safety");
expect(row?.action).toContain("or export OPENCODEX_API_AUTH_TOKEN");
expect(`${lines.join("\n")}\n${row?.action}`.split("codex-shell-env.sh")).toHaveLength(2);
}));

test("healthy legacy guidance remains visible without an env_key warning", () => {
const shim = { ...healthyShim, summary: "Legacy Unix shim installed in place; run ocx codex-shim install." };
expect(collectCodexEnvKeyReadiness(config, {}, shim, true)).toBeNull();
expect(formatCodexShimDoctorLines(shim)).toEqual([` ${shim.summary}`]);
});

test("Windows, missing, damaged, and active shims receive no Unix activation hint", () => {
const inactive = { installed: true, healthy: false, runnable: true, active: false, summary: "inactive" };
expect(formatCodexShimDoctorLines(inactive, "win32")).toEqual([]);
expect(formatCodexShimDoctorLines(missingShim)).toEqual([]);
expect(formatCodexShimDoctorLines({ ...inactive, runnable: false })).toEqual([]);
expect(formatCodexShimDoctorLines({ ...inactive, active: true, healthy: true })).toEqual([]);
});
});
Loading