@minhspark/codex-mcp-bridge 1.19.3 → 1.19.5

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/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](ht
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.19.5] - 2026-10-02
8
+
9
+ ### Added
10
+
11
+ - Add an explicit `--full-access` installer option that sets Codex Full access + Never globally, repairs the managed policy on Windows, macOS, and Linux with OS administrator authentication when needed, and restores the chosen settings on later bridge connections.
12
+
13
+ ## [1.19.4] - 2026-10-02
14
+
15
+ ### Fixed
16
+
17
+ - Detect a managed Codex `requirements.toml` whose `allowed_sandbox_modes` omits `read-only` before auto-starting an external app-server. The bridge and `npm run check` now report the policy error directly instead of waiting for a generic connection failure; they do not edit the administrator-managed file.
18
+
7
19
  ## [1.19.3] - 2026-09-30
8
20
 
9
21
  ### Changed
package/README.md CHANGED
@@ -51,6 +51,10 @@ Choose the mode for the conversations you want to connect:
51
51
 
52
52
  The bridge requires **Node.js 22+**; Node 24 LTS is a suitable starting point. If Node is already managed by a version manager, use that installation. Install the bridge under the same OS user as the clients. A global npm install does not require cloning this repository.
53
53
 
54
+ To opt a machine you administer into **Full access + Never**, run `codex-mcp-bridge-install --full-access` when registering the bridge (`codex-mcp-bridge-install.cmd --full-access` in PowerShell). The installer sets global Codex defaults for old and new projects, repairs or creates the managed policy with both `read-only` and `danger-full-access`, and records the choice so the bridge can restore it if the files drift later. Windows UAC, macOS administrator authentication, or Linux polkit/sudo may be required for the system policy. Normal installation does not change Codex permissions. Run `codex doctor --summary --ascii` after setup; existing Desktop tasks may need to be reopened to load the new permissions.
55
+
56
+ If the bridge is already registered, `codex-full-access` (`codex-full-access.cmd` in PowerShell) enables the same settings without replacing that registration. The bridge reads the saved choice when it starts.
57
+
54
58
  For Desktop mode, install [Codex Desktop](https://developers.openai.com/codex/app) and [Claude Desktop](https://claude.com/download), sign in, and save the intended local project in Codex Desktop. Open that same directory in Claude Desktop's Code tab. A normal Claude chat is not a Code session.
55
59
 
56
60
  ### Windows (PowerShell)
@@ -310,6 +314,7 @@ Start with `codex doctor` for Codex installation problems and `claude doctor` fo
310
314
 
311
315
  | Error / symptom | Fix |
312
316
  |---|---|
317
+ | Codex says `Organization settings could not be loaded`, or the bridge reports `INVALID_MANAGED_CONFIG` | On a machine you administer, rerun `codex-mcp-bridge-install --full-access` to repair the managed policy and global defaults. The policy must include `"read-only"` alongside `"danger-full-access"`; the former is required for Codex to load it, while the effective mode remains Full access. Complete any OS administrator prompt, then run `codex doctor --summary --ascii`. |
313
318
  | `codex binary not found`, `ENOENT`, or Windows `EINVAL` during registration | Locate the actual executable. Set `CODEX_EXE` before rerunning the installer: PowerShell `$env:CODEX_EXE = (Get-Command codex.exe).Source`; macOS/Linux `export CODEX_EXE="$(command -v codex)"`. On Windows, do not point it at `codex.ps1` or `codex.cmd`. |
314
319
  | Tools appear in Claude Desktop but not in its Code task | Complete the separate [Claude Code registration](#register-claude-code), then reconnect `/mcp` in that Code session. |
315
320
  | Installer refuses an entry with custom access/timeout settings | Keep those settings. Update only the existing entry's `command` and `args` to the values printed by the installer, then reconnect. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhspark/codex-mcp-bridge",
3
- "version": "1.19.3",
3
+ "version": "1.19.5",
4
4
  "description": "Two-way MCP bridge between Claude and Codex: prompts into a live Codex thread, messages into a running Claude Code session.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -28,7 +28,8 @@
28
28
  "claude-mcp-bridge-install": "scripts/install-codex-mcp.mjs",
29
29
  "codex-native-relay": "src/native-relay-launcher.mjs",
30
30
  "codex-native-relay-install": "scripts/install-native-relay.mjs",
31
- "codex-npm-footer-install": "scripts/install-npm-footer.mjs"
31
+ "codex-npm-footer-install": "scripts/install-npm-footer.mjs",
32
+ "codex-full-access": "scripts/repair-codex-access.mjs"
32
33
  },
33
34
  "files": [
34
35
  "src",
package/scripts/check.mjs CHANGED
@@ -2,6 +2,8 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
2
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { readManagedRequirementsIssue } from "../src/app-server-client.mjs";
6
+ import { enableCodexFullAccess, fullAccessEnabled } from "../src/codex-full-access.mjs";
5
7
  import { desktopTasksConfigured } from "../src/native-relay.mjs";
6
8
 
7
9
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -15,6 +17,7 @@ const bridgeEnvNames = [
15
17
  "CODEX_BRIDGE_APPROVAL",
16
18
  "CODEX_BRIDGE_AUTO_APPROVE_ACK",
17
19
  "CODEX_BRIDGE_APPROVAL_POLICY",
20
+ "CODEX_BRIDGE_ENFORCE_FULL_ACCESS",
18
21
  "CODEX_BRIDGE_AUTOSTART",
19
22
  "CODEX_BRIDGE_DESKTOP_TASKS",
20
23
  "CODEX_NATIVE_RELAY_SOCKET",
@@ -40,6 +43,9 @@ const transport = new StdioClientTransport({
40
43
  const client = new Client({ name: "bridge-check", version: "1.0.0" });
41
44
 
42
45
  try {
46
+ if (fullAccessEnabled()) enableCodexFullAccess();
47
+ const managedIssue = readManagedRequirementsIssue();
48
+ if (managedIssue) throw new Error(managedIssue);
43
49
  await client.connect(transport);
44
50
  const tools = await client.listTools();
45
51
  console.log("tools:", tools.tools.map((t) => t.name).join(", "));
@@ -7,6 +7,7 @@ import { IS_WINDOWS, PLATFORM_LABEL, claudeDesktopConfigPath, resolveCodexBin }
7
7
  import { desktopTasksConfigured } from "../src/native-relay.mjs";
8
8
  import { exitForVersionRequest } from "../src/cli-version.mjs";
9
9
  import { createReleaseSnapshot, snapshotRoot } from "../src/release-snapshot.mjs";
10
+ import { enableCodexFullAccess, fullAccessEnabled } from "../src/codex-full-access.mjs";
10
11
 
11
12
  exitForVersionRequest(import.meta.url);
12
13
 
@@ -14,6 +15,8 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
14
15
  const cfgPath = process.env.CLAUDE_DESKTOP_CONFIG ?? claudeDesktopConfigPath();
15
16
  const reset = process.argv.includes("--reset");
16
17
  const forceDesktopTasks = process.argv.includes("--desktop-tasks");
18
+ const fullAccess = process.argv.includes("--full-access") || fullAccessEnabled();
19
+ if (fullAccess) enableCodexFullAccess();
17
20
 
18
21
  /**
19
22
  * This bridge is intentionally the local hand-off point between Claude and
@@ -85,7 +88,8 @@ cfg.mcpServers["codex-bridge"] = {
85
88
  CODEX_BRIDGE_ALLOWED_ROOTS: settled("CODEX_BRIDGE_ALLOWED_ROOTS", defaultRoots),
86
89
  CODEX_BRIDGE_APPROVAL: settled("CODEX_BRIDGE_APPROVAL", "deny"),
87
90
  CODEX_BRIDGE_AUTO_APPROVE_ACK: settled("CODEX_BRIDGE_AUTO_APPROVE_ACK", "0"),
88
- CODEX_BRIDGE_APPROVAL_POLICY: settled("CODEX_BRIDGE_APPROVAL_POLICY", "on-request"),
91
+ CODEX_BRIDGE_APPROVAL_POLICY: fullAccess ? "never" : settled("CODEX_BRIDGE_APPROVAL_POLICY", "on-request"),
92
+ CODEX_BRIDGE_ENFORCE_FULL_ACCESS: fullAccess ? "1" : settled("CODEX_BRIDGE_ENFORCE_FULL_ACCESS", "0"),
89
93
  /**
90
94
  * Written out even at its default so it is visible in the file. Left
91
95
  * implicit, the one setting that decides whether the bridge can reach a
@@ -94,7 +98,7 @@ cfg.mcpServers["codex-bridge"] = {
94
98
  * points nowhere near it.
95
99
  */
96
100
  CODEX_BRIDGE_THREAD_POLICY: settled("CODEX_BRIDGE_THREAD_POLICY", "roots"),
97
- CODEX_BRIDGE_SANDBOX: settled("CODEX_BRIDGE_SANDBOX", "workspace-write"),
101
+ CODEX_BRIDGE_SANDBOX: fullAccess ? "danger-full-access" : settled("CODEX_BRIDGE_SANDBOX", "workspace-write"),
98
102
  CODEX_BRIDGE_OPEN_IN_APP: settled("CODEX_BRIDGE_OPEN_IN_APP", IS_WINDOWS ? "1" : "0"),
99
103
  CODEX_BRIDGE_RELEASE_AFTER_TURN: settled("CODEX_BRIDGE_RELEASE_AFTER_TURN", IS_WINDOWS ? "1" : "0"),
100
104
  ...(desktopMode !== undefined ? { CODEX_BRIDGE_DESKTOP_TASKS: desktopMode } : {}),
@@ -9,6 +9,7 @@ import { exitForVersionRequest } from "../src/cli-version.mjs";
9
9
  import { desktopTasksConfigured } from "../src/native-relay.mjs";
10
10
  import { codexMcpRegistration } from "../src/codex-mcp-registration.mjs";
11
11
  import { createReleaseSnapshot, snapshotRoot } from "../src/release-snapshot.mjs";
12
+ import { enableCodexFullAccess, fullAccessEnabled } from "../src/codex-full-access.mjs";
12
13
 
13
14
  exitForVersionRequest(import.meta.url);
14
15
 
@@ -18,6 +19,7 @@ const serverName = process.env.CLAUDE_BRIDGE_NAME ?? "claude-bridge";
18
19
  const entry = path.join(root, "src", "mcp-supervisor.mjs");
19
20
  const entryArgs = ["claude-bridge.mjs"];
20
21
  const remove = process.argv.includes("--remove");
22
+ if (!remove && (process.argv.includes("--full-access") || fullAccessEnabled())) enableCodexFullAccess();
21
23
  const permissionMode = process.env.CLAUDE_BRIDGE_PERMISSION_MODE;
22
24
  if (!remove && permissionMode && !["bypass", "prompting"].includes(permissionMode)) throw new Error("CLAUDE_BRIDGE_PERMISSION_MODE must be bypass or prompting");
23
25
 
@@ -19,15 +19,17 @@ import {
19
19
  import { exitForVersionRequest } from "../src/cli-version.mjs";
20
20
  import { stdioMcpRegistration } from "../src/codex-mcp-registration.mjs";
21
21
  import { createReleaseSnapshot, snapshotRoot } from "../src/release-snapshot.mjs";
22
+ import { enableCodexFullAccess, fullAccessEnabled } from "../src/codex-full-access.mjs";
22
23
 
23
24
  exitForVersionRequest(import.meta.url);
24
25
 
25
- const VERSION = "1.19.3";
26
+ const VERSION = "1.19.5";
26
27
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
27
28
  const entry = path.join(root, "src", "mcp-supervisor.mjs");
28
29
  const entryArgs = ["native-relay-companion.mjs"];
29
30
  const serverName = process.env.CODEX_NATIVE_RELAY_NAME ?? "codex-native-relay";
30
31
  const remove = process.argv.includes("--remove");
32
+ if (!remove && (process.argv.includes("--full-access") || fullAccessEnabled())) enableCodexFullAccess();
31
33
  const skipBootstrap = process.argv.includes("--no-bootstrap");
32
34
 
33
35
  const codexBin = resolveCodexBin(process.env.CODEX_EXE);
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import { applyFullAccessPolicy, enableCodexFullAccess } from "../src/codex-full-access.mjs";
3
+ import { exitForVersionRequest } from "../src/cli-version.mjs";
4
+
5
+ exitForVersionRequest(import.meta.url);
6
+
7
+ try {
8
+ if (process.argv.includes("--elevated")) {
9
+ applyFullAccessPolicy();
10
+ console.log("Codex managed policy repaired");
11
+ } else {
12
+ const paths = enableCodexFullAccess();
13
+ console.log(`Codex Full access + Never enabled: ${paths.config}`);
14
+ console.log(`Managed policy: ${paths.policy}`);
15
+ }
16
+ } catch (error) {
17
+ console.error(`Codex access repair failed: ${error.message}`);
18
+ process.exitCode = 1;
19
+ }
@@ -1,13 +1,34 @@
1
1
  import { execFileSync, spawn } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import path from "node:path";
2
4
  import { setTimeout as delay } from "node:timers/promises";
3
5
 
4
6
  import { PLATFORM_LABEL, resolveCodexBin, spawnEnv } from "./platform.mjs";
5
7
  import { assertAllowedAppServerUrl } from "./security-policy.mjs";
8
+ import { enableCodexFullAccess, fullAccessEnabled } from "./codex-full-access.mjs";
6
9
 
7
10
  const DEFAULT_URL = "ws://127.0.0.1:8791";
8
11
  const CONNECT_ATTEMPTS = 2;
9
12
  const CONNECT_RETRY_DELAY_MS = 750;
10
13
 
14
+ function managedRequirementsPath() {
15
+ return process.platform === "win32"
16
+ ? path.join(process.env.ProgramData ?? process.env.PROGRAMDATA ?? "C:\\ProgramData", "OpenAI", "Codex", "requirements.toml")
17
+ : "/etc/codex/requirements.toml";
18
+ }
19
+
20
+ export function readManagedRequirementsIssue(filePath = managedRequirementsPath()) {
21
+ let contents;
22
+ try {
23
+ contents = readFileSync(filePath, "utf8");
24
+ } catch {
25
+ return null;
26
+ }
27
+ const sandboxModes = /^\s*allowed_sandbox_modes\s*=\s*\[([\s\S]*?)\]/m.exec(contents);
28
+ if (!sandboxModes || /["']read-only["']/.test(sandboxModes[1])) return null;
29
+ return `Codex managed requirements at ${filePath} are invalid: allowed_sandbox_modes must include "read-only". Ask the machine administrator to repair the policy, then run codex doctor. The bridge does not edit managed policy files.`;
30
+ }
31
+
11
32
  export function parseListeningPids(output, port) {
12
33
  const suffix = `:${port}`;
13
34
  return [
@@ -81,6 +102,7 @@ export class CodexAppServerClient {
81
102
  this.url = options.url ?? process.env.CODEX_APP_SERVER_URL ?? DEFAULT_URL;
82
103
  assertAllowedAppServerUrl(this.url);
83
104
  this.codexBin = resolveCodexBin(options.codexBin);
105
+ this.requirementsPath = options.requirementsPath ?? managedRequirementsPath();
84
106
  this.autoStart = options.autoStart ?? process.env.CODEX_BRIDGE_AUTOSTART !== "0";
85
107
  this.log = options.log ?? (() => {});
86
108
  const requestedApproval = options.approval ?? process.env.CODEX_BRIDGE_APPROVAL ?? "deny";
@@ -119,6 +141,8 @@ export class CodexAppServerClient {
119
141
  }
120
142
 
121
143
  async startServer() {
144
+ const managedIssue = readManagedRequirementsIssue(this.requirementsPath);
145
+ if (managedIssue) throw new AppServerError(managedIssue, "INVALID_MANAGED_CONFIG");
122
146
  this.log(`starting shared app-server on ${PLATFORM_LABEL}: ${this.codexBin} app-server --listen ${this.url}`);
123
147
  const needsShell = /\.(cmd|bat)$/i.test(this.codexBin);
124
148
  const child = spawn(this.codexBin, ["app-server", "--listen", this.url], {
@@ -188,12 +212,20 @@ export class CodexAppServerClient {
188
212
  for (let attempt = 1; attempt <= CONNECT_ATTEMPTS; attempt += 1) {
189
213
  try {
190
214
  this.#assertConnectionEpoch(epoch);
215
+ if (fullAccessEnabled()) {
216
+ try {
217
+ enableCodexFullAccess();
218
+ } catch (error) {
219
+ throw new AppServerError(`Codex Full access repair failed: ${error.message}`, "CODEX_ACCESS_REPAIR_FAILED");
220
+ }
221
+ }
191
222
  await this.#openConnection(epoch);
192
223
  return;
193
224
  } catch (err) {
194
225
  lastError = err;
195
226
  this.#assertConnectionEpoch(epoch);
196
227
  this.log(`connect attempt ${attempt}/${CONNECT_ATTEMPTS} failed: ${err.message}`);
228
+ if (["INVALID_MANAGED_CONFIG", "CODEX_ACCESS_REPAIR_FAILED"].includes(err.code)) throw err;
197
229
  if (attempt < CONNECT_ATTEMPTS) await delay(CONNECT_RETRY_DELAY_MS);
198
230
  }
199
231
  }
@@ -26,7 +26,7 @@ import { AGENT_PROMPT_GUIDANCE, PROMPT_FIELD_HINT } from "./prompt-guidance.mjs"
26
26
 
27
27
  exitForVersionRequest(import.meta.url);
28
28
 
29
- const VERSION = "1.19.3";
29
+ const VERSION = "1.19.5";
30
30
  void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
31
31
  const FORWARD_MIN_INTERVAL_MS = 5000;
32
32
  const FORWARD_MAX_PER_SESSION = 50;
@@ -0,0 +1,175 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const POLICY_HEADER = [
8
+ 'default_permissions = ":danger-full-access"',
9
+ 'allowed_approval_policies = ["never"]',
10
+ 'allowed_approvals_reviewers = ["user"]',
11
+ 'allowed_sandbox_modes = ["read-only", "danger-full-access"]',
12
+ ];
13
+ const CONFIG_HEADER = [
14
+ 'approval_policy = "never"',
15
+ 'sandbox_mode = "danger-full-access"',
16
+ ];
17
+
18
+ export function codexAccessPaths(env = process.env) {
19
+ const home = env.CODEX_HOME || path.join(os.homedir(), ".codex");
20
+ return {
21
+ config: path.join(home, "config.toml"),
22
+ marker: path.join(home, "bridge-full-access.enabled"),
23
+ policy: process.platform === "win32"
24
+ ? path.join(env.ProgramData || env.PROGRAMDATA || "C:\\ProgramData", "OpenAI", "Codex", "requirements.toml")
25
+ : "/etc/codex/requirements.toml",
26
+ };
27
+ }
28
+
29
+ function newlineFor(contents) {
30
+ return contents.includes("\r\n") ? "\r\n" : "\n";
31
+ }
32
+
33
+ function replaceTopLevel(contents, keys, header) {
34
+ const newline = newlineFor(contents);
35
+ const firstTable = /^[ \t]*\[[^\]\r\n]+\]/m.exec(contents);
36
+ const splitAt = firstTable?.index ?? contents.length;
37
+ const top = contents.slice(0, splitAt).replace(
38
+ new RegExp(`^[ \\t]*(?:${keys.join("|")})[ \\t]*=[^\\r\\n]*(?:\\r?\\n|$)`, "gm"),
39
+ "",
40
+ );
41
+ const rest = contents.slice(splitAt);
42
+ return `${header.join(newline)}${newline}${top.trim() ? `${top.trimStart()}${top.endsWith(newline) ? "" : newline}` : newline}${rest}`;
43
+ }
44
+
45
+ export function fullAccessConfigContents(contents = "") {
46
+ const bom = contents.startsWith("\uFEFF") ? "\uFEFF" : "";
47
+ const source = contents.slice(bom.length);
48
+ const firstTable = /^[ \t]*\[[^\]\r\n]+\]/m.exec(source);
49
+ const top = source.slice(0, firstTable?.index ?? source.length);
50
+ if (["approval_policy", "sandbox_mode"].every((key, index) =>
51
+ top.split(/\r?\n/).filter((entry) => new RegExp(`^[ \\t]*${key}[ \\t]*=`).test(entry)).length === 1 &&
52
+ top.split(/\r?\n/).some((entry) => entry.trim() === CONFIG_HEADER[index]))) return contents;
53
+ return bom + replaceTopLevel(source, ["approval_policy", "sandbox_mode"], CONFIG_HEADER);
54
+ }
55
+
56
+ export function fullAccessPolicyContents(contents = "") {
57
+ const bom = contents.startsWith("\uFEFF") ? "\uFEFF" : "";
58
+ const newline = newlineFor(contents);
59
+ let updated = replaceTopLevel(
60
+ contents.slice(bom.length),
61
+ ["default_permissions", "allowed_approval_policies", "allowed_approvals_reviewers", "allowed_sandbox_modes"],
62
+ POLICY_HEADER,
63
+ );
64
+ const table = /^[ \t]*\[allowed_permission_profiles\][ \t]*(?:#.*)?$/m.exec(updated);
65
+ if (!table) {
66
+ updated = `${updated.trimEnd()}${newline}${newline}[allowed_permission_profiles]${newline}":danger-full-access" = true${newline}`;
67
+ } else {
68
+ const bodyStart = table.index + table[0].length;
69
+ const nextTable = /^[ \t]*\[[^\]\r\n]+\]/m.exec(updated.slice(bodyStart));
70
+ const bodyEnd = nextTable ? bodyStart + nextTable.index : updated.length;
71
+ const body = updated.slice(bodyStart, bodyEnd)
72
+ .replace(/^[ \t]*["']?:danger-full-access["']?[ \t]*=[^\r\n]*(?:\r?\n|$)/gm, "")
73
+ .replace(/^(?:\r?\n)+/, "");
74
+ updated = `${updated.slice(0, bodyStart)}${newline}":danger-full-access" = true${newline}${body}${updated.slice(bodyEnd)}`;
75
+ }
76
+ return bom + updated;
77
+ }
78
+
79
+ function writeIfChanged(filePath, contents, transform) {
80
+ const next = transform(contents);
81
+ if (next === contents) return false;
82
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
83
+ fs.writeFileSync(filePath, next, { encoding: "utf8", mode: 0o600 });
84
+ return true;
85
+ }
86
+
87
+ function withUnlockedPolicy(filePath, action) {
88
+ if (!fs.existsSync(filePath)) return action();
89
+ if (process.platform === "darwin") {
90
+ const flags = execFileSync("/usr/bin/stat", ["-f", "%Sf", filePath], { encoding: "utf8" }).trim();
91
+ if (flags.split(",").includes("uchg")) {
92
+ execFileSync("/usr/bin/chflags", ["nouchg", filePath]);
93
+ try { return action(); } finally { execFileSync("/usr/bin/chflags", ["uchg", filePath]); }
94
+ }
95
+ }
96
+ if (process.platform === "linux") {
97
+ let attributes = "";
98
+ try {
99
+ attributes = execFileSync("lsattr", ["-d", filePath], { encoding: "utf8" }).split(/\s+/)[0];
100
+ } catch {}
101
+ if (attributes.includes("i")) {
102
+ execFileSync("chattr", ["-i", filePath]);
103
+ try { return action(); } finally { execFileSync("chattr", ["+i", filePath]); }
104
+ }
105
+ }
106
+ return action();
107
+ }
108
+
109
+ export function applyFullAccessPolicy(filePath = codexAccessPaths().policy) {
110
+ if (fs.existsSync(filePath) && fs.lstatSync(filePath).isSymbolicLink()) {
111
+ throw new Error(`Refusing to rewrite a symbolic-link managed policy: ${filePath}`);
112
+ }
113
+ const previous = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
114
+ const next = fullAccessPolicyContents(previous);
115
+ if (next === previous) return false;
116
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
117
+ return withUnlockedPolicy(filePath, () => writeIfChanged(filePath, previous, fullAccessPolicyContents));
118
+ }
119
+
120
+ function shellQuote(value) {
121
+ return `'${value.replaceAll("'", "'\\''")}'`;
122
+ }
123
+
124
+ function powershellQuote(value) {
125
+ return `'${value.replaceAll("'", "''")}'`;
126
+ }
127
+
128
+ function elevatedRepair() {
129
+ const helper = fileURLToPath(new URL("../scripts/repair-codex-access.mjs", import.meta.url));
130
+ if (process.platform === "win32") {
131
+ const command = `& ${powershellQuote(process.execPath)} ${powershellQuote(helper)} --elevated; exit $LASTEXITCODE`;
132
+ const encoded = Buffer.from(command, "utf16le").toString("base64");
133
+ const wrapper = `$p = Start-Process -FilePath 'powershell.exe' -ArgumentList @('-NoProfile','-EncodedCommand','${encoded}') -Verb RunAs -Wait -PassThru -WindowStyle Hidden; exit $p.ExitCode`;
134
+ execFileSync("powershell.exe", ["-NoProfile", "-EncodedCommand", Buffer.from(wrapper, "utf16le").toString("base64")], { windowsHide: true, timeout: 120_000 });
135
+ return;
136
+ }
137
+ if (process.platform === "darwin") {
138
+ try {
139
+ execFileSync("sudo", ["-n", process.execPath, helper, "--elevated"], { timeout: 120_000 });
140
+ return;
141
+ } catch {}
142
+ const command = `${shellQuote(process.execPath)} ${shellQuote(helper)} --elevated`;
143
+ const appleScript = `do shell script "${command.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}" with administrator privileges`;
144
+ execFileSync("osascript", ["-e", appleScript], { timeout: 120_000 });
145
+ return;
146
+ }
147
+ try {
148
+ execFileSync("sudo", ["-n", process.execPath, helper, "--elevated"], { timeout: 120_000 });
149
+ } catch {
150
+ if (process.stdin.isTTY) {
151
+ execFileSync("sudo", [process.execPath, helper, "--elevated"], { stdio: "inherit", timeout: 120_000 });
152
+ } else {
153
+ execFileSync("pkexec", [process.execPath, helper, "--elevated"], { timeout: 120_000 });
154
+ }
155
+ }
156
+ }
157
+
158
+ export function fullAccessEnabled(paths = codexAccessPaths()) {
159
+ return fs.existsSync(paths.marker);
160
+ }
161
+
162
+ export function enableCodexFullAccess({ paths = codexAccessPaths(), allowElevation = true } = {}) {
163
+ const config = fs.existsSync(paths.config) ? fs.readFileSync(paths.config, "utf8") : "";
164
+ writeIfChanged(paths.config, config, fullAccessConfigContents);
165
+ try {
166
+ applyFullAccessPolicy(paths.policy);
167
+ } catch (error) {
168
+ if (!allowElevation || paths.policy !== codexAccessPaths().policy) throw error;
169
+ elevatedRepair();
170
+ const current = fs.readFileSync(paths.policy, "utf8");
171
+ if (fullAccessPolicyContents(current) !== current) throw new Error("Codex managed policy remains incomplete after administrator repair");
172
+ }
173
+ if (!fs.existsSync(paths.marker)) fs.writeFileSync(paths.marker, "enabled\n", { mode: 0o600 });
174
+ return paths;
175
+ }
package/src/index.mjs CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  } from "./platform.mjs";
24
24
  import { runTurn } from "./turn.mjs";
25
25
  import { BridgeSecurityPolicy } from "./security-policy.mjs";
26
+ import { enableCodexFullAccess, fullAccessEnabled } from "./codex-full-access.mjs";
26
27
  import { DesktopTaskDelivery, DESKTOP_TOOL_BUDGET_MS } from "./thread-delivery.mjs";
27
28
  import { desktopTasksConfigured } from "./native-relay.mjs";
28
29
  import { exitForVersionRequest } from "./cli-version.mjs";
@@ -34,7 +35,7 @@ import { AGENT_PROMPT_GUIDANCE, PROMPT_FIELD_HINT } from "./prompt-guidance.mjs"
34
35
 
35
36
  exitForVersionRequest(import.meta.url);
36
37
 
37
- const VERSION = "1.19.3";
38
+ const VERSION = "1.19.5";
38
39
  void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
39
40
  const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
40
41
 
@@ -53,7 +54,18 @@ const DEFAULT_RELEASE_AFTER_TURN = process.env.CODEX_BRIDGE_RELEASE_AFTER_TURN
53
54
  : IS_WINDOWS;
54
55
  const TERMINAL_TURN_STATUSES = new Set(["completed", "interrupted", "failed"]);
55
56
  const RELEASE_TURN_STATUSES = TERMINAL_TURN_STATUSES;
56
- const security = new BridgeSecurityPolicy();
57
+ const enforceFullAccess = fullAccessEnabled();
58
+ if (enforceFullAccess) {
59
+ try {
60
+ enableCodexFullAccess();
61
+ } catch (error) {
62
+ log(`Codex Full access repair failed: ${error.message}`);
63
+ }
64
+ }
65
+ const security = new BridgeSecurityPolicy({
66
+ ...process.env,
67
+ ...(enforceFullAccess ? { CODEX_BRIDGE_ENFORCE_FULL_ACCESS: "1" } : {}),
68
+ });
57
69
  const desktopTasksEnabled = desktopTasksConfigured();
58
70
  const runtime = createRuntimeState({ configuration: desktopTasksConfigured });
59
71
  const desktopOperation = new AsyncLocalStorage();
@@ -34,7 +34,7 @@ import { createHardenedRootPolicy } from "./hardened-root-policy.mjs";
34
34
 
35
35
  exitForVersionRequest(import.meta.url);
36
36
 
37
- const VERSION = "1.19.3";
37
+ const VERSION = "1.19.5";
38
38
  const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
39
39
 
40
40
  function errorResponse(code, message, sent) {
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
  import { createHardenedRootPolicy } from "./hardened-root-policy.mjs";
4
4
 
5
5
  const APPROVAL_POLICIES = new Set(["untrusted", "on-failure", "on-request", "never"]);
6
- const SANDBOXES = new Set(["read-only", "workspace-write"]);
6
+ const SANDBOXES = new Set(["read-only", "workspace-write", "danger-full-access"]);
7
7
  const THREAD_POLICIES = new Set(["owned", "roots"]);
8
8
  const ALLOW_ALL_ROOTS = "*";
9
9
  const ALLOW_ALL_THREADS = "*";
@@ -118,14 +118,15 @@ export class BridgeSecurityPolicy {
118
118
  throw new Error(`CODEX_BRIDGE_THREAD_POLICY must be owned or roots: ${this.threadPolicy}`);
119
119
  }
120
120
 
121
- this.approvalPolicy = env.CODEX_BRIDGE_APPROVAL_POLICY ?? "on-request";
121
+ const fullAccess = env.CODEX_BRIDGE_ENFORCE_FULL_ACCESS === "1";
122
+ this.approvalPolicy = fullAccess ? "never" : env.CODEX_BRIDGE_APPROVAL_POLICY ?? "on-request";
122
123
  if (!APPROVAL_POLICIES.has(this.approvalPolicy)) {
123
124
  throw new Error(`Invalid CODEX_BRIDGE_APPROVAL_POLICY: ${this.approvalPolicy}`);
124
125
  }
125
126
 
126
- this.sandbox = env.CODEX_BRIDGE_SANDBOX ?? "workspace-write";
127
+ this.sandbox = fullAccess ? "danger-full-access" : env.CODEX_BRIDGE_SANDBOX ?? "workspace-write";
127
128
  if (!SANDBOXES.has(this.sandbox)) {
128
- throw new Error(`CODEX_BRIDGE_SANDBOX must be read-only or workspace-write: ${this.sandbox}`);
129
+ throw new Error(`CODEX_BRIDGE_SANDBOX must be read-only, workspace-write, or danger-full-access: ${this.sandbox}`);
129
130
  }
130
131
  }
131
132