@bitkyc08/opencodex 2.6.21 → 2.6.22-preview.20260705

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,7 +16,7 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-npFbiPU_.js"></script>
19
+ <script type="module" crossorigin src="/assets/index-BKfmXUNz.js"></script>
20
20
  <link rel="stylesheet" crossorigin href="/assets/index-DDcEW0Cm.css">
21
21
  </head>
22
22
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.6.21",
3
+ "version": "2.6.22-preview.20260705",
4
4
  "description": "Universal provider proxy for OpenAI Codex — use any LLM with Codex CLI/App/SDK",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -33,7 +33,7 @@ import {
33
33
  import { debugProviderDiagnostic } from "../../debug";
34
34
  import { mcpArgsFromToolCall } from "./protobuf-events";
35
35
  import { OCX_RESPONSES_TOOL_PROVIDER } from "./tool-definitions";
36
- import { handleCursorNativeExec, handleCursorNativeKv, type CursorNativeExecContext } from "./native-exec";
36
+ import { cursorUnsafeNativeLocalExecEnabled, handleCursorNativeExec, handleCursorNativeKv, type CursorNativeExecContext } from "./native-exec";
37
37
  import { resolveMcpServers } from "./mcp-config";
38
38
  import { CursorMcpManager } from "./mcp-manager";
39
39
  import { buildMcpToolDefinitions, mcpDepsFromManager } from "./native-exec-mcp";
@@ -333,7 +333,7 @@ class LiveCursorTransport implements CursorTransport {
333
333
  this.activeClientToolFinalizeGraceMs = this.clientToolFinalizeGraceMs;
334
334
  // Desktop (computer-use / record-screen) executors are available even with no MCP servers.
335
335
  this.desktopDeps = desktopDepsFromConfig(input.provider.desktopExecutor);
336
- this.execContext = { ...this.desktopDeps };
336
+ this.execContext = { ...this.desktopDeps, unsafeAllowNativeLocalExec: cursorUnsafeNativeLocalExecEnabled(input.provider) };
337
337
  const servers = resolveMcpServers(input.provider);
338
338
  if (servers.length > 0) {
339
339
  this.mcpManager = new CursorMcpManager(servers, {
@@ -354,10 +354,15 @@ class LiveCursorTransport implements CursorTransport {
354
354
  this.mcpPrepared = (async () => {
355
355
  try {
356
356
  const mcpToolDefs = await buildMcpToolDefinitions(this.mcpManager!);
357
- this.execContext = { ...this.desktopDeps, ...mcpDepsFromManager(this.mcpManager!), mcpToolDefs };
357
+ this.execContext = {
358
+ ...this.desktopDeps,
359
+ ...mcpDepsFromManager(this.mcpManager!),
360
+ mcpToolDefs,
361
+ unsafeAllowNativeLocalExec: cursorUnsafeNativeLocalExecEnabled(this.input.provider),
362
+ };
358
363
  } catch (err) {
359
364
  console.warn(`[cursor-mcp] preparation failed, MCP disabled for this stream: ${err instanceof Error ? err.message : String(err)}`);
360
- this.execContext = { ...this.desktopDeps };
365
+ this.execContext = { ...this.desktopDeps, unsafeAllowNativeLocalExec: cursorUnsafeNativeLocalExecEnabled(this.input.provider) };
361
366
  }
362
367
  })();
363
368
  }
@@ -43,6 +43,17 @@ function codexNativeMutationRefusal(operation: "write" | "delete"): string {
43
43
  return `Cursor-native ${operation} is disabled for this Codex request because apply_patch is available. Use the apply_patch tool for file edits so Codex can approve the change, enforce sandbox policy, show diffs, and record rollout. No file was changed.`;
44
44
  }
45
45
 
46
+ const NATIVE_LOCAL_EXEC_DISABLED =
47
+ "Cursor native local filesystem execution is disabled by default because it bypasses Codex approval and sandbox enforcement. Set provider.unsafeAllowNativeLocalExec=true only for trusted local experiments that may read or mutate local files directly.";
48
+
49
+ export function rejectReadExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
50
+ if (execMsg.message.case !== "readArgs") throw new Error("invalid read exec");
51
+ const path = resolve(execMsg.message.value.path);
52
+ return execBytes(execMsg, "readResult", create(ReadResultSchema, {
53
+ result: { case: "error", value: create(ReadErrorSchema, { path, error: NATIVE_LOCAL_EXEC_DISABLED }) },
54
+ }));
55
+ }
56
+
46
57
  export function readExec(execMsg: ExecServerMessage): Uint8Array {
47
58
  if (execMsg.message.case !== "readArgs") throw new Error("invalid read exec");
48
59
  const path = resolve(execMsg.message.value.path);
@@ -84,6 +95,17 @@ export function rejectWriteExecForApplyPatch(execMsg: ExecServerMessage): Uint8A
84
95
  }));
85
96
  }
86
97
 
98
+ export function rejectWriteExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
99
+ if (execMsg.message.case !== "writeArgs") throw new Error("invalid write exec");
100
+ const path = resolve(execMsg.message.value.path);
101
+ return execBytes(execMsg, "writeResult", create(WriteResultSchema, {
102
+ result: {
103
+ case: "rejected",
104
+ value: create(WriteRejectedSchema, { path, reason: `${NATIVE_LOCAL_EXEC_DISABLED} No file was changed.` }),
105
+ },
106
+ }));
107
+ }
108
+
87
109
  export function writeExec(execMsg: ExecServerMessage): Uint8Array {
88
110
  if (execMsg.message.case !== "writeArgs") throw new Error("invalid write exec");
89
111
  const args = execMsg.message.value;
@@ -122,6 +144,17 @@ export function rejectDeleteExecForApplyPatch(execMsg: ExecServerMessage): Uint8
122
144
  }));
123
145
  }
124
146
 
147
+ export function rejectDeleteExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
148
+ if (execMsg.message.case !== "deleteArgs") throw new Error("invalid delete exec");
149
+ const path = resolve(execMsg.message.value.path);
150
+ return execBytes(execMsg, "deleteResult", create(DeleteResultSchema, {
151
+ result: {
152
+ case: "rejected",
153
+ value: create(DeleteRejectedSchema, { path, reason: `${NATIVE_LOCAL_EXEC_DISABLED} No file was changed.` }),
154
+ },
155
+ }));
156
+ }
157
+
125
158
  export function deleteExec(execMsg: ExecServerMessage): Uint8Array {
126
159
  if (execMsg.message.case !== "deleteArgs") throw new Error("invalid delete exec");
127
160
  const path = resolve(execMsg.message.value.path);
@@ -152,6 +185,14 @@ export function deleteExec(execMsg: ExecServerMessage): Uint8Array {
152
185
  }
153
186
  }
154
187
 
188
+ export function rejectLsExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
189
+ if (execMsg.message.case !== "lsArgs") throw new Error("invalid ls exec");
190
+ const path = resolve(execMsg.message.value.path);
191
+ return execBytes(execMsg, "lsResult", create(LsResultSchema, {
192
+ result: { case: "error", value: create(LsErrorSchema, { path, error: NATIVE_LOCAL_EXEC_DISABLED }) },
193
+ }));
194
+ }
195
+
155
196
  export function lsExec(execMsg: ExecServerMessage): Uint8Array {
156
197
  if (execMsg.message.case !== "lsArgs") throw new Error("invalid ls exec");
157
198
  const path = resolve(execMsg.message.value.path);
@@ -212,6 +253,10 @@ function grepError(execMsg: ExecServerMessage, error: string): Uint8Array {
212
253
  }));
213
254
  }
214
255
 
256
+ export function rejectGrepExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
257
+ return grepError(execMsg, NATIVE_LOCAL_EXEC_DISABLED);
258
+ }
259
+
215
260
  export function grepExec(execMsg: ExecServerMessage): Uint8Array {
216
261
  if (execMsg.message.case !== "grepArgs") throw new Error("invalid grep exec");
217
262
  const args: GrepArgs = execMsg.message.value;
@@ -6,6 +6,17 @@ export interface CursorNativeNetworkDeps {
6
6
  fetch?: typeof fetch;
7
7
  }
8
8
 
9
+ const NATIVE_FETCH_DISABLED =
10
+ "Cursor native fetch execution is disabled by default because it bypasses Codex approval and sandbox enforcement. Set provider.unsafeAllowNativeLocalExec=true only for trusted local experiments that may make local network requests directly.";
11
+
12
+ export function rejectFetchExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
13
+ if (execMsg.message.case !== "fetchArgs") throw new Error("invalid fetch exec");
14
+ const args = execMsg.message.value;
15
+ return execBytes(execMsg, "fetchResult", create(FetchResultSchema, {
16
+ result: { case: "error", value: create(FetchErrorSchema, { url: args.url, error: NATIVE_FETCH_DISABLED }) },
17
+ }));
18
+ }
19
+
9
20
  export async function fetchExec(execMsg: ExecServerMessage, deps: CursorNativeNetworkDeps = {}): Promise<Uint8Array> {
10
21
  if (execMsg.message.case !== "fetchArgs") throw new Error("invalid fetch exec");
11
22
  const args = execMsg.message.value;
@@ -23,6 +23,33 @@ import { errorText, execBytes, execStreamCloseBytes } from "./native-exec-common
23
23
  const backgroundShells = new Map<number, { child: ChildProcessWithoutNullStreams; outputLength: number }>();
24
24
  let nextShellId = 1;
25
25
 
26
+ const NATIVE_SHELL_DISABLED =
27
+ "Cursor native shell execution is disabled by default because it bypasses Codex approval and sandbox enforcement. Set provider.unsafeAllowNativeLocalExec=true only for trusted local experiments that may run local commands directly.";
28
+
29
+ function rejectedShellResult(command: string, cwd: string, started: number) {
30
+ return create(ShellResultSchema, {
31
+ result: {
32
+ case: "failure",
33
+ value: create(ShellFailureSchema, {
34
+ command,
35
+ workingDirectory: cwd,
36
+ exitCode: 1,
37
+ signal: "",
38
+ stdout: "",
39
+ stderr: NATIVE_SHELL_DISABLED,
40
+ executionTime: Date.now() - started,
41
+ aborted: true,
42
+ }),
43
+ },
44
+ });
45
+ }
46
+
47
+ export function rejectShellExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
48
+ if (execMsg.message.case !== "shellArgs") throw new Error("invalid shell exec");
49
+ const args = execMsg.message.value;
50
+ return execBytes(execMsg, "shellResult", rejectedShellResult(args.command, resolve(args.workingDirectory || process.cwd()), Date.now()));
51
+ }
52
+
26
53
  export function shellExec(execMsg: ExecServerMessage): Uint8Array {
27
54
  if (execMsg.message.case !== "shellArgs") throw new Error("invalid shell exec");
28
55
  const args = execMsg.message.value;
@@ -58,6 +85,26 @@ export function shellExec(execMsg: ExecServerMessage): Uint8Array {
58
85
  }));
59
86
  }
60
87
 
88
+ export function rejectShellStreamExecForPolicy(execMsg: ExecServerMessage): Uint8Array[] {
89
+ if (execMsg.message.case !== "shellStreamArgs") throw new Error("invalid shell stream exec");
90
+ const args = execMsg.message.value;
91
+ const cwd = resolve(args.workingDirectory || process.cwd());
92
+ const started = Date.now();
93
+ return [
94
+ execBytes(execMsg, "shellStream", create(ShellStreamSchema, {
95
+ event: { case: "start", value: create(ShellStreamStartSchema, { sandboxPolicy: args.requestedSandboxPolicy }) },
96
+ })),
97
+ execBytes(execMsg, "shellStream", create(ShellStreamSchema, {
98
+ event: { case: "stderr", value: create(ShellStreamStderrSchema, { data: NATIVE_SHELL_DISABLED }) },
99
+ })),
100
+ execBytes(execMsg, "shellStream", create(ShellStreamSchema, {
101
+ event: { case: "exit", value: create(ShellStreamExitSchema, { code: 1, cwd, aborted: true }) },
102
+ })),
103
+ execBytes(execMsg, "shellResult", rejectedShellResult(args.command, cwd, started)),
104
+ execStreamCloseBytes(execMsg),
105
+ ];
106
+ }
107
+
61
108
  export async function shellStreamExec(execMsg: ExecServerMessage): Promise<Uint8Array[]> {
62
109
  if (execMsg.message.case !== "shellStreamArgs") throw new Error("invalid shell stream exec");
63
110
  const args = execMsg.message.value;
@@ -144,6 +191,15 @@ export async function shellStreamExec(execMsg: ExecServerMessage): Promise<Uint8
144
191
  return replies;
145
192
  }
146
193
 
194
+ export function rejectBackgroundShellSpawnExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
195
+ if (execMsg.message.case !== "backgroundShellSpawnArgs") throw new Error("invalid background shell exec");
196
+ const args = execMsg.message.value;
197
+ const cwd = resolve(args.workingDirectory || process.cwd());
198
+ return execBytes(execMsg, "backgroundShellSpawnResult", create(BackgroundShellSpawnResultSchema, {
199
+ result: { case: "error", value: create(BackgroundShellSpawnErrorSchema, { command: args.command, workingDirectory: cwd, error: NATIVE_SHELL_DISABLED }) },
200
+ }));
201
+ }
202
+
147
203
  export function backgroundShellSpawnExec(execMsg: ExecServerMessage): Uint8Array {
148
204
  if (execMsg.message.case !== "backgroundShellSpawnArgs") throw new Error("invalid background shell exec");
149
205
  const args = execMsg.message.value;
@@ -174,6 +230,13 @@ export function backgroundShellSpawnExec(execMsg: ExecServerMessage): Uint8Array
174
230
  }
175
231
  }
176
232
 
233
+ export function rejectWriteShellStdinExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
234
+ if (execMsg.message.case !== "writeShellStdinArgs") throw new Error("invalid shell stdin exec");
235
+ return execBytes(execMsg, "writeShellStdinResult", create(WriteShellStdinResultSchema, {
236
+ result: { case: "error", value: create(WriteShellStdinErrorSchema, { error: NATIVE_SHELL_DISABLED }) },
237
+ }));
238
+ }
239
+
177
240
  export function writeShellStdinExec(execMsg: ExecServerMessage): Uint8Array {
178
241
  if (execMsg.message.case !== "writeShellStdinArgs") throw new Error("invalid shell stdin exec");
179
242
  const args = execMsg.message.value;
@@ -14,9 +14,31 @@ import {
14
14
  type ExecServerMessage,
15
15
  type KvServerMessage,
16
16
  } from "./gen/agent_pb";
17
- import { deleteExec, grepExec, lsExec, readExec, rejectDeleteExecForApplyPatch, rejectWriteExecForApplyPatch, writeExec } from "./native-exec-fs";
18
- import { fetchExec, type CursorNativeNetworkDeps } from "./native-exec-network";
19
- import { backgroundShellSpawnExec, shellExec, shellStreamExec, writeShellStdinExec } from "./native-exec-shell";
17
+ import {
18
+ deleteExec,
19
+ grepExec,
20
+ lsExec,
21
+ readExec,
22
+ rejectDeleteExecForApplyPatch,
23
+ rejectDeleteExecForPolicy,
24
+ rejectGrepExecForPolicy,
25
+ rejectLsExecForPolicy,
26
+ rejectReadExecForPolicy,
27
+ rejectWriteExecForApplyPatch,
28
+ rejectWriteExecForPolicy,
29
+ writeExec,
30
+ } from "./native-exec-fs";
31
+ import { fetchExec, rejectFetchExecForPolicy, type CursorNativeNetworkDeps } from "./native-exec-network";
32
+ import {
33
+ backgroundShellSpawnExec,
34
+ rejectBackgroundShellSpawnExecForPolicy,
35
+ rejectShellExecForPolicy,
36
+ rejectShellStreamExecForPolicy,
37
+ rejectWriteShellStdinExecForPolicy,
38
+ shellExec,
39
+ shellStreamExec,
40
+ writeShellStdinExec,
41
+ } from "./native-exec-shell";
20
42
  import {
21
43
  computerUseExec,
22
44
  listMcpResourcesExec,
@@ -39,10 +61,18 @@ export type CursorNativeExecDeps = CursorNativeNetworkDeps & CursorNativeToolDep
39
61
  export interface CursorNativeExecContext extends CursorNativeExecDeps {
40
62
  mcpToolDefs?: McpToolDefinition[];
41
63
  clientToolDefs?: McpToolDefinition[];
64
+ /** Unsafe opt-in escape hatch for Cursor server-driven local fs/shell/fetch execution. */
65
+ unsafeAllowNativeLocalExec?: boolean;
66
+ /** @deprecated Use unsafeAllowNativeLocalExec. Kept as a transition alias for local experiments. */
67
+ allowNativeLocalExec?: boolean;
42
68
  /** apply_patch is visible for this request; Cursor-native write/delete must not bypass Codex. */
43
69
  rejectNativeFileMutations?: boolean;
44
70
  }
45
71
 
72
+ export function cursorUnsafeNativeLocalExecEnabled(input: Pick<CursorNativeExecContext, "unsafeAllowNativeLocalExec" | "allowNativeLocalExec"> = {}): boolean {
73
+ return input.unsafeAllowNativeLocalExec === true || input.allowNativeLocalExec === true;
74
+ }
75
+
46
76
  /**
47
77
  * Content-addressed blob store shared across streams. Bounded: without eviction a long-running
48
78
  * proxy accumulates every conversation's prompt blobs forever (unbounded memory) and any stale
@@ -115,6 +145,18 @@ export async function handleCursorNativeExec(execMsg: ExecServerMessage, deps: C
115
145
  result: { case: "success", value: create(RequestContextSuccessSchema, { requestContext: create(RequestContextSchema, { tools }) }) },
116
146
  }))];
117
147
  }
148
+ if (!cursorUnsafeNativeLocalExecEnabled(deps)) {
149
+ if (execCase === "readArgs") return [rejectReadExecForPolicy(execMsg)];
150
+ if (execCase === "writeArgs") return [rejectWriteExecForPolicy(execMsg)];
151
+ if (execCase === "deleteArgs") return [rejectDeleteExecForPolicy(execMsg)];
152
+ if (execCase === "lsArgs") return [rejectLsExecForPolicy(execMsg)];
153
+ if (execCase === "grepArgs") return [rejectGrepExecForPolicy(execMsg)];
154
+ if (execCase === "shellArgs") return [rejectShellExecForPolicy(execMsg)];
155
+ if (execCase === "shellStreamArgs") return rejectShellStreamExecForPolicy(execMsg);
156
+ if (execCase === "backgroundShellSpawnArgs") return [rejectBackgroundShellSpawnExecForPolicy(execMsg)];
157
+ if (execCase === "writeShellStdinArgs") return [rejectWriteShellStdinExecForPolicy(execMsg)];
158
+ if (execCase === "fetchArgs") return [rejectFetchExecForPolicy(execMsg)];
159
+ }
118
160
  if (execCase === "readArgs") return [readExec(execMsg)];
119
161
  if (execCase === "writeArgs") return [deps.rejectNativeFileMutations ? rejectWriteExecForApplyPatch(execMsg) : writeExec(execMsg)];
120
162
  if (execCase === "deleteArgs") return [deps.rejectNativeFileMutations ? rejectDeleteExecForApplyPatch(execMsg) : deleteExec(execMsg)];
package/src/cli-help.ts CHANGED
@@ -37,9 +37,12 @@ const helpEntries: Record<string, HelpEntry> = {
37
37
  details: ["Alias of: ocx uninstall"],
38
38
  },
39
39
  service: {
40
- usage: "ocx service <install|start|stop|status|uninstall|remove>",
40
+ usage: "ocx service [install|start|stop|status|uninstall|remove]",
41
41
  summary: "Run as a background service.",
42
- details: ["Use `ocx service status` to see diagnostics and log paths."],
42
+ details: [
43
+ "With no subcommand, installs/updates and starts the background service.",
44
+ "Use `ocx service status` to see diagnostics and log paths.",
45
+ ],
43
46
  },
44
47
  "codex-shim": {
45
48
  usage: "ocx codex-shim <install|status|uninstall|remove>",
@@ -82,7 +85,7 @@ Usage:
82
85
  ocx recover-history --legacy-openai
83
86
  Explicitly recover pre-backup syncResumeHistory rows
84
87
  ocx uninstall Remove service/shim/config and restore native Codex (alias: remove)
85
- ocx service <sub> Run as a background service (install|start|stop|status|uninstall|remove)
88
+ ocx service [sub] Run as a background service (default: install/update/start)
86
89
  ocx codex-shim <sub> Auto-start proxy when \`codex\` launches (install|status|uninstall|remove)
87
90
  ocx ensure Ensure the proxy is running and Codex config/cache are current
88
91
  ocx sync Fetch models from providers and inject into Codex config
@@ -1,13 +1,13 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import os from "node:os";
4
3
  import { getCodexAccountCredential } from "./codex-account-store";
5
4
  import { loadConfig } from "./config";
5
+ import { resolveCodexHomeDir } from "./codex-home";
6
6
  import { extractAccountId } from "./oauth/chatgpt";
7
7
 
8
8
  export function readCodexTokens(): { access_token: string; account_id: string; id_token?: string } | null {
9
9
  try {
10
- const codexHome = process.env["CODEX_HOME"] || join(os.homedir(), ".codex");
10
+ const codexHome = resolveCodexHomeDir();
11
11
  const authPath = join(codexHome, "auth.json");
12
12
  if (!existsSync(authPath)) return null;
13
13
  const j = JSON.parse(readFileSync(authPath, "utf-8")) as {
@@ -0,0 +1,91 @@
1
+ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ import { expandUserPath } from "./config";
5
+
6
+ export type CodexHomeDeps = {
7
+ env?: NodeJS.ProcessEnv;
8
+ platform?: NodeJS.Platform | string;
9
+ release?: string;
10
+ procVersion?: string | null;
11
+ homedir?: () => string;
12
+ usersRoot?: string;
13
+ existsSync?: (path: string) => boolean;
14
+ readdirSync?: (path: string) => string[];
15
+ statSync?: typeof statSync;
16
+ realpathSync?: (path: string) => string;
17
+ };
18
+
19
+ function windowsUserProfileToWslPath(value: string | undefined): string | null {
20
+ if (!value) return null;
21
+ const normalized = value.replaceAll("\\", "/");
22
+ const match = normalized.match(/^([A-Za-z]):\/Users\/([^/]+)$/);
23
+ if (!match) return null;
24
+ return `/mnt/${match[1]!.toLowerCase()}/Users/${match[2]}`;
25
+ }
26
+
27
+ function readProcVersion(): string | null {
28
+ try {
29
+ return readFileSync("/proc/version", "utf8");
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ export function isWslRuntime(deps: CodexHomeDeps = {}): boolean {
36
+ const env = deps.env ?? process.env;
37
+ if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) return true;
38
+ if ((deps.platform ?? process.platform) !== "linux") return false;
39
+ const version = `${deps.release ?? ""}\n${deps.procVersion ?? readProcVersion() ?? ""}`;
40
+ return /microsoft|wsl/i.test(version);
41
+ }
42
+
43
+ export function findWslWindowsCodexHome(deps: CodexHomeDeps = {}): string | null {
44
+ if (!isWslRuntime(deps)) return null;
45
+ const exists = deps.existsSync ?? existsSync;
46
+ const stat = deps.statSync ?? statSync;
47
+ const readdir = deps.readdirSync ?? readdirSync;
48
+ const realpath = deps.realpathSync ?? realpathSync.native;
49
+ const env = deps.env ?? process.env;
50
+ const usersRoot = deps.usersRoot ?? "/mnt/c/Users";
51
+ if (!exists(usersRoot)) return null;
52
+
53
+ const explicitProfile = windowsUserProfileToWslPath(env.USERPROFILE);
54
+ const candidates = [];
55
+ try {
56
+ for (const user of readdir(usersRoot)) {
57
+ if (user === "Default" || user === "Default User" || user === "Public" || user === "All Users") continue;
58
+ const home = join(usersRoot, user, ".codex");
59
+ const config = join(home, "config.toml");
60
+ if (!exists(config)) continue;
61
+ try {
62
+ if (stat(home).isDirectory()) candidates.push(realpath(home));
63
+ } catch {
64
+ // Ignore unreadable Windows profiles.
65
+ }
66
+ }
67
+ } catch {
68
+ return null;
69
+ }
70
+
71
+ if (explicitProfile) {
72
+ const explicitHome = join(explicitProfile, ".codex");
73
+ const match = candidates.find(candidate => candidate === explicitHome || candidate.endsWith(`/${explicitProfile.split("/").pop()}/.codex`));
74
+ if (match) return match;
75
+ }
76
+ return candidates.length === 1 ? candidates[0]! : null;
77
+ }
78
+
79
+ export function defaultCodexHome(deps: CodexHomeDeps = {}): string {
80
+ const home = (deps.homedir ?? homedir)();
81
+ const defaultHome = join(home, ".codex");
82
+ const exists = deps.existsSync ?? existsSync;
83
+ const detected = !exists(join(defaultHome, "config.toml")) ? findWslWindowsCodexHome(deps) : null;
84
+ return detected ?? defaultHome;
85
+ }
86
+
87
+ export function resolveCodexHomeDir(deps: CodexHomeDeps = {}): string {
88
+ const raw = (deps.env ?? process.env).CODEX_HOME?.trim();
89
+ if (raw) return resolve(expandUserPath(raw));
90
+ return defaultCodexHome(deps);
91
+ }
@@ -1,7 +1,7 @@
1
1
  import { realpathSync, statSync } from "node:fs";
2
- import { homedir } from "node:os";
3
2
  import { isAbsolute, join, resolve } from "node:path";
4
3
  import { expandUserPath } from "./config";
4
+ import { defaultCodexHome } from "./codex-home";
5
5
 
6
6
  function resolveCodexHome(): string {
7
7
  const raw = process.env.CODEX_HOME?.trim();
@@ -20,7 +20,7 @@ function resolveCodexHome(): string {
20
20
  return realpathSync.native(path);
21
21
  }
22
22
 
23
- return join(homedir(), ".codex");
23
+ return defaultCodexHome();
24
24
  }
25
25
 
26
26
  export const CODEX_HOME = resolveCodexHome();
package/src/doctor.ts CHANGED
@@ -8,25 +8,19 @@
8
8
  * networking. See devlog/_plan/260630_wsl-account-autoswitch/30_*.
9
9
  */
10
10
  import { existsSync, readFileSync } from "node:fs";
11
- import { homedir } from "node:os";
12
- import { join, resolve } from "node:path";
13
- import { expandUserPath, getConfigDir, getConfigPath, readConfigDiagnostics, readPid, resolveEnvValue } from "./config";
11
+ import { join } from "node:path";
12
+ import { getConfigDir, getConfigPath, readConfigDiagnostics, readPid, resolveEnvValue } from "./config";
14
13
  import { readCodexTokens } from "./codex-auth-collision";
14
+ import { resolveCodexHomeDir as resolveCodexHomeDirImpl } from "./codex-home";
15
+ export { resolveCodexHomeDir } from "./codex-home";
15
16
 
16
17
  const WHAM_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
17
18
  const PROBE_TIMEOUT_MS = 8000;
18
19
 
19
20
  export type PathRow = { label: string; path: string; exists: boolean };
20
21
 
21
- export function resolveCodexHomeDir(): string {
22
- const raw = process.env["CODEX_HOME"]?.trim();
23
- // `~` parity with the hardened runtime paths (codex-paths.ts) — a literal "~/..." here
24
- // would report every Codex file as missing while the runtime happily uses the real dir.
25
- return raw ? resolve(expandUserPath(raw)) : join(homedir(), ".codex");
26
- }
27
-
28
22
  export function collectPaths(): PathRow[] {
29
- const codexHome = resolveCodexHomeDir();
23
+ const codexHome = resolveCodexHomeDirImpl();
30
24
  const opencodexHome = getConfigDir();
31
25
  return [
32
26
  { label: "CODEX_HOME", path: codexHome, exists: existsSync(codexHome) },
@@ -135,13 +135,13 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
135
135
  },
136
136
  {
137
137
  id: "cursor",
138
- label: "Cursor (experimental, live native exec)",
138
+ label: "Cursor (experimental)",
139
139
  adapter: "cursor",
140
140
  baseUrl: "https://api2.cursor.sh",
141
141
  authKind: "oauth",
142
142
  featured: false,
143
143
  dashboardPreset: true,
144
- note: "Experimental Cursor bridge. Live transport, live model discovery, and native read/write/delete/shell execution are enabled after a standalone PKCE browser login via 'ocx login cursor'.",
144
+ note: "Experimental Cursor bridge. Live transport and live model discovery are enabled after a standalone PKCE browser login via 'ocx login cursor'; native read/write/delete/shell/fetch execution stays disabled unless provider.unsafeAllowNativeLocalExec is explicitly set for a trusted local experiment.",
145
145
  models: cursorModelIds(CURSOR_STATIC_MODELS),
146
146
  liveModels: true,
147
147
  defaultModel: "auto",
package/src/service.ts CHANGED
@@ -538,7 +538,15 @@ function installSystemd(): void {
538
538
  sh(`systemctl --user restart ${TASK}`);
539
539
  writeServiceInstallState();
540
540
  }
541
- function startSystemd(): void { sh(`systemctl --user start ${TASK}`); }
541
+ function startSystemd(): void {
542
+ ensureUserBusEnv();
543
+ if (!existsSync(unitPath())) {
544
+ console.error(`opencodex service is not installed: ${unitPath()}`);
545
+ console.error("Run `ocx service install` first to create and enable the systemd user unit.");
546
+ process.exit(1);
547
+ }
548
+ sh(`systemctl --user start ${TASK}`);
549
+ }
542
550
  function stopSystemd(): void { try { sh(`systemctl --user stop ${TASK}`); } catch { /* not running */ } }
543
551
  function statusSystemd(): string { try { return sh(`systemctl --user status ${TASK}`); } catch { return ""; } }
544
552
  function uninstallSystemd(): void {
@@ -674,13 +682,18 @@ export function serviceStatusSummary(): string {
674
682
  return `unsupported on ${process.platform}`;
675
683
  }
676
684
 
685
+ export function normalizeServiceSubcommand(sub?: string): string {
686
+ return sub ?? "install";
687
+ }
688
+
677
689
  export async function serviceCommand(sub?: string): Promise<void> {
678
690
  const ops = platformOps();
679
691
  if (!ops) {
680
692
  console.error("ocx service supports macOS (launchd), Windows (Task Scheduler), and Linux (systemd).");
681
693
  process.exit(1);
682
694
  }
683
- switch (sub) {
695
+ const command = normalizeServiceSubcommand(sub);
696
+ switch (command) {
684
697
  case "install":
685
698
  assertServiceEnvironmentMatchesInstall();
686
699
  assertServiceAuthEnvironment();
@@ -717,7 +730,8 @@ export async function serviceCommand(sub?: string): Promise<void> {
717
730
  console.log("✅ service uninstalled + native Codex restored.");
718
731
  break;
719
732
  default:
720
- console.error("Usage: ocx service <install|start|stop|status|uninstall|remove>");
733
+ console.error("Usage: ocx service [install|start|stop|status|uninstall|remove]");
734
+ console.error(" With no subcommand, installs/updates and starts the background service.");
721
735
  process.exit(1);
722
736
  }
723
737
  }
package/src/types.ts CHANGED
@@ -425,6 +425,15 @@ export interface OcxProviderConfig {
425
425
  * that can. With no executor, these tools honestly report "not supported".
426
426
  */
427
427
  desktopExecutor?: import("./adapters/cursor/native-exec-desktop").DesktopExecutorConfig;
428
+ /**
429
+ * Cursor adapter only: unsafe opt-in escape hatch for Cursor server-driven built-in local
430
+ * read/write/delete/ls/grep/shell/fetch execution. Defaults to false so remote Cursor messages
431
+ * cannot bypass Codex approval/sandbox semantics. Explicit MCP and desktop executors remain
432
+ * controlled by their own opt-in config.
433
+ */
434
+ unsafeAllowNativeLocalExec?: boolean;
435
+ /** @deprecated Use unsafeAllowNativeLocalExec. Kept as a transition alias for local experiments. */
436
+ allowNativeLocalExec?: boolean;
428
437
  }
429
438
 
430
439
  export interface CodexAccount {