@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.
- package/README.ko.md +2 -2
- package/README.md +12 -9
- package/README.zh-CN.md +2 -2
- package/gui/dist/assets/{index-npFbiPU_.js → index-BKfmXUNz.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/cursor/live-transport.ts +9 -4
- package/src/adapters/cursor/native-exec-fs.ts +45 -0
- package/src/adapters/cursor/native-exec-network.ts +11 -0
- package/src/adapters/cursor/native-exec-shell.ts +63 -0
- package/src/adapters/cursor/native-exec.ts +45 -3
- package/src/cli-help.ts +6 -3
- package/src/codex-auth-collision.ts +2 -2
- package/src/codex-home.ts +91 -0
- package/src/codex-paths.ts +2 -2
- package/src/doctor.ts +5 -11
- package/src/providers/registry.ts +2 -2
- package/src/service.ts +17 -3
- package/src/types.ts +9 -0
package/gui/dist/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
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
|
@@ -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 = {
|
|
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 {
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
40
|
+
usage: "ocx service [install|start|stop|status|uninstall|remove]",
|
|
41
41
|
summary: "Run as a background service.",
|
|
42
|
-
details: [
|
|
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
|
|
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 =
|
|
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
|
+
}
|
package/src/codex-paths.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
12
|
-
import {
|
|
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 =
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 {
|