codex-control-chrome-mcp 1.0.0 → 1.1.0

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 ADDED
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ This project follows semantic versioning.
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [1.1.0] - 2026-06-27
10
+
11
+ ### Added
12
+
13
+ - Self-healing native host registration: the MCP stdio server re-asserts the manifest on startup when a Codex update reverts it (opt out with `CODEX_CONTROL_CHROME_NO_AUTO_REGISTER=1`).
14
+ - Robust identification of OpenAI's genuine host via stable signals (manifest name + extension origin, bundle path, binary format, and the macOS OpenAI code signature) instead of the binary filename.
15
+ - `status` now reports `registered` and a `classification` of the live manifest/host.
16
+
17
+ ### Fixed
18
+
19
+ - Proxy target resolution preferred a stale recorded path over the live manifest, so a renamed/removed official host (e.g. `extension-host` → `Codex for Chrome`) could be recorded as the proxy target. It now prefers the live official manifest and skips paths that no longer exist.
20
+
21
+ ## [1.0.1] - 2026-06-26
22
+
23
+ ### Added
24
+
25
+ - Open source governance files.
26
+ - CI and npm trusted publishing workflows.
27
+ - Node.js test coverage for framing, RPC, and installer behavior.
28
+
29
+ ### Changed
30
+
31
+ - Runtime package name and version are now read from `package.json` instead of being hard-coded in implementation files.
32
+
33
+ ## [1.0.0] - 2026-06-26
34
+
35
+ ### Added
36
+
37
+ - Initial npm package.
38
+ - MCP stdio server.
39
+ - Chrome Native Messaging host bridge.
40
+ - Native host install and uninstall commands with manifest backup.
41
+ - Basic Chrome tab, navigation, screenshot, CDP, and event MCP tools.
42
+ - Skill documentation for Agent tool usage.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 iola1999
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,55 +1,167 @@
1
1
  # Codex Control Chrome MCP
2
2
 
3
- Expose the Codex Chrome extension flow to other Agent tools through MCP.
3
+ [![npm version](https://img.shields.io/npm/v/codex-control-chrome-mcp.svg)](https://www.npmjs.com/package/codex-control-chrome-mcp)
4
+ [![CI](https://github.com/iola1999/codex-control-chrome-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/iola1999/codex-control-chrome-mcp/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
6
+
7
+ Expose the Codex Chrome Extension flow to other Agent tools through MCP.
8
+
9
+ This project lets MCP clients control the user's normal Chrome profile through the installed [Codex Chrome Extension](https://chromewebstore.google.com/detail/hehggadaopoacecdllhhajmbjkdcmajg). It is useful when an Agent needs existing tabs, cookies, logged-in sessions, installed extensions, screenshots, console/network events, or raw Chrome DevTools Protocol commands.
10
+
11
+ This is an independent community project. It is not affiliated with OpenAI, Codex, Google, or Chrome.
4
12
 
5
13
  - NPM: [codex-control-chrome-mcp](https://www.npmjs.com/package/codex-control-chrome-mcp)
6
14
  - GitHub: [iola1999/codex-control-chrome-mcp](https://github.com/iola1999/codex-control-chrome-mcp)
7
15
  - Chrome Extension: [Codex Chrome Extension](https://chromewebstore.google.com/detail/hehggadaopoacecdllhhajmbjkdcmajg)
8
16
  - Skill: [`skills/codex-control-chrome-mcp/SKILL.md`](./skills/codex-control-chrome-mcp/SKILL.md)
9
17
 
10
- Current implementation is an early Node.js prototype with:
18
+ ## Status
19
+
20
+ The current implementation provides:
11
21
 
12
22
  - official `@modelcontextprotocol/sdk` stdio server
13
23
  - Chrome Native Messaging host mode
14
- - MCP stdio server mode
15
24
  - install/uninstall commands with manifest backup
16
25
  - JSON-RPC bridge socket under `/tmp/codex-browser-use`
17
- - basic Chrome tab/CDP MCP tools
26
+ - Chrome tab, navigation, screenshot, CDP, and event MCP tools
27
+ - bundled skill documentation for Agent tool ordering
18
28
 
19
- Install the Chrome native host:
29
+ ## Platform Support
20
30
 
21
- ```bash
22
- npx -y codex-control-chrome-mcp@latest install-native-host
23
- ```
31
+ | Platform | Status | Notes |
32
+ | --- | --- | --- |
33
+ | macOS | Tested | Primary supported platform. |
34
+ | Linux | Experimental | Manifest path targets Google Chrome under `~/.config/google-chrome`. |
35
+ | Windows | Unsupported | Native Messaging registration uses registry keys on Windows and is not implemented yet. |
24
36
 
25
- The user must also have the [Codex Chrome Extension](https://chromewebstore.google.com/detail/hehggadaopoacecdllhhajmbjkdcmajg) installed and enabled in the Chrome profile they want to automate.
37
+ Node.js 20 or newer is required. npm Trusted Publishing for releases uses Node.js 24 in GitHub Actions.
26
38
 
27
- Uninstall and restore the previous Codex native host manifest:
39
+ ## Quick Start
40
+
41
+ Install and enable the [Codex Chrome Extension](https://chromewebstore.google.com/detail/hehggadaopoacecdllhhajmbjkdcmajg) in the Chrome profile you want to automate.
42
+
43
+ Install the Chrome native host:
28
44
 
29
45
  ```bash
30
- npx -y codex-control-chrome-mcp@latest uninstall-native-host
46
+ npx -y codex-control-chrome-mcp@latest install-native-host
31
47
  ```
32
48
 
33
- Intended Agent config:
49
+ Configure your Agent to use the MCP stdio server:
34
50
 
35
51
  ```json
36
52
  {
37
53
  "type": "stdio",
38
54
  "command": "npx",
39
- "args": ["-y", "codex-control-chrome-mcp@latest", "--stdio"]
55
+ "args": ["-y", "codex-control-chrome-mcp@latest", "--stdio"],
56
+ "startup_timeout_sec": 30
40
57
  }
41
58
  ```
42
59
 
60
+ Check status:
61
+
62
+ ```bash
63
+ npx -y codex-control-chrome-mcp@latest status
64
+ ```
65
+
66
+ Uninstall and restore the previous Codex native host manifest:
67
+
68
+ ```bash
69
+ npx -y codex-control-chrome-mcp@latest uninstall-native-host
70
+ ```
71
+
72
+ ## Agent Skill
73
+
43
74
  If your Agent supports skills, install or reference the bundled skill folder:
44
75
 
45
76
  ```text
46
77
  skills/codex-control-chrome-mcp
47
78
  ```
48
79
 
49
- The skill documents the recommended MCP tool order for status checks, tab claiming, CDP commands, screenshots, events, and tab finalization.
80
+ The skill documents the recommended MCP tool order for:
81
+
82
+ - status checks
83
+ - tab listing and claiming
84
+ - CDP attach and command execution
85
+ - screenshots
86
+ - network and console event reads
87
+ - tab finalization
50
88
 
51
- Read:
89
+ ## Security Notes
90
+
91
+ This project controls the user's normal Chrome profile. MCP clients connected to it can inspect page contents and send raw CDP commands to claimed tabs.
92
+
93
+ Only install and run it on machines and Chrome profiles you own or are explicitly authorized to automate. Do not send browser cookies, password stores, profile databases, tokens, or private session files in issues or logs.
94
+
95
+ See [SECURITY.md](./SECURITY.md) for the security model and reporting process.
96
+
97
+ ## How It Works
98
+
99
+ The Codex Chrome Extension does not need an external Chrome remote debugging port. It uses Chrome Native Messaging and the extension's `chrome.debugger` permission:
100
+
101
+ ```text
102
+ Chrome extension
103
+ -> chrome.runtime.connectNative("com.openai.codexextension")
104
+ -> codex-control-chrome-mcp native host
105
+ -> local MCP bridge socket
106
+ -> chrome.debugger.attach / chrome.debugger.sendCommand
107
+ -> CDP
108
+ ```
109
+
110
+ Because the control entrypoint is inside the normal Chrome profile, existing cookies, logged-in sessions, tabs, and extensions can be reused.
111
+
112
+ Read more:
52
113
 
53
114
  - [Architecture](./docs/architecture.md)
54
115
  - [Install And Uninstall](./docs/install.md)
55
116
  - [Official Socket Probe Notes](./docs/socket-probe.md)
117
+ - [Release Process](./docs/release.md)
118
+
119
+ ## Development
120
+
121
+ Install dependencies:
122
+
123
+ ```bash
124
+ npm ci
125
+ ```
126
+
127
+ Run checks:
128
+
129
+ ```bash
130
+ npm run ci
131
+ ```
132
+
133
+ Run the MCP server locally:
134
+
135
+ ```bash
136
+ node ./bin/codex-control-chrome-mcp.js --stdio
137
+ ```
138
+
139
+ Run native host mode locally for development:
140
+
141
+ ```bash
142
+ node ./bin/codex-control-chrome-mcp.js --native-host
143
+ ```
144
+
145
+ ## Troubleshooting
146
+
147
+ `status` reports a `registered` flag and a `classification` of the live manifest. When `registered` is `false` and `classification.kind` is `official-openai`, a Codex update has reclaimed the manifest (see below). `classification` identifies OpenAI's genuine host by stable signals — the `com.openai.codexextension` name, the official extension origin, the bundle path, the binary format, and (on macOS) the OpenAI code signature — not by filename.
148
+
149
+ ### A Codex update stopped the bridge
150
+
151
+ Codex App/extension updates re-register their own `com.openai.codexextension` native-messaging manifest, which overwrites this project's proxy registration (the bundled host binary has even been renamed across releases). The symptom is `status` showing `sockets: []`, `registered: false`, and a manifest `path` back inside the Codex bundle.
152
+
153
+ The MCP stdio server now **self-heals**: on startup it re-asserts the manifest when it detects it was reverted, so you usually do not need to re-run `install-native-host`. Because Chrome only reads the manifest when the extension reconnects, **reload the Codex Chrome Extension (or restart Chrome) once** after a Codex update to bring the bridge back. Set `CODEX_CONTROL_CHROME_NO_AUTO_REGISTER=1` to disable the auto re-register.
154
+
155
+ If `status` shows no bridge sockets, reload Chrome or the Codex Chrome Extension after installing the native host.
156
+
157
+ If Codex App integration stops working, uninstall this native host to restore the backed-up manifest:
158
+
159
+ ```bash
160
+ npx -y codex-control-chrome-mcp@latest uninstall-native-host
161
+ ```
162
+
163
+ If tab or CDP tools fail, use the bundled skill workflow: list tabs again, claim only current tab IDs, attach before raw CDP calls, and read CDP events after enabling the relevant domain.
164
+
165
+ ## License
166
+
167
+ MIT
package/SECURITY.md ADDED
@@ -0,0 +1,37 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ Security fixes are provided for the latest published npm version.
6
+
7
+ ## Reporting a Vulnerability
8
+
9
+ Please report security issues privately by emailing `iola1999@foxmail.com`.
10
+
11
+ Include:
12
+
13
+ - affected version
14
+ - operating system
15
+ - Chrome version and profile type
16
+ - MCP client name and version
17
+ - reproduction steps
18
+ - relevant `codex-control-chrome-mcp status` output with sensitive paths or data redacted
19
+
20
+ Do not include browser cookies, password-store data, session databases, or other private Chrome profile files in reports.
21
+
22
+ ## Security Model
23
+
24
+ This project controls the user's normal Chrome profile through the Codex Chrome Extension and Chrome DevTools Protocol. That can expose sensitive browser state, including logged-in sessions and page contents.
25
+
26
+ The trusted boundary is the local user account. Only install and run this tool on machines and Chrome profiles you own or are explicitly authorized to automate.
27
+
28
+ This project is not affiliated with OpenAI, Codex, Google, or Chrome. It reuses the installed Codex Chrome Extension native messaging host name so the extension can connect to this local bridge.
29
+
30
+ ## Known Security Tradeoffs
31
+
32
+ - The native host manifest for `com.openai.codexextension` is replaced during install and restored during uninstall.
33
+ - The local bridge socket is intended for same-user local access only.
34
+ - MCP clients connected to this server can send raw CDP commands to claimed tabs.
35
+ - The project does not attempt to enforce per-site or per-command policy.
36
+
37
+ For operational details, see `docs/architecture.md` and `docs/install.md`.
package/docs/install.md CHANGED
@@ -17,6 +17,16 @@ The current implementation provides:
17
17
 
18
18
  Do not run install on a machine where Codex Chrome integration is important unless you are ready to test proxy mode or restore from backup.
19
19
 
20
+ This is an independent community project. It is not affiliated with OpenAI, Codex, Google, or Chrome.
21
+
22
+ ## Platform Support
23
+
24
+ | Platform | Status | Notes |
25
+ | --- | --- | --- |
26
+ | macOS | Tested | Primary supported platform. |
27
+ | Linux | Experimental | Uses the Google Chrome user-level Native Messaging host directory. |
28
+ | Windows | Unsupported | Chrome Native Messaging registration uses registry keys on Windows and is not implemented yet. |
29
+
20
30
  ## Install
21
31
 
22
32
  First install and enable the [Codex Chrome Extension](https://chromewebstore.google.com/detail/hehggadaopoacecdllhhajmbjkdcmajg) in the Chrome profile you want to automate.
@@ -43,6 +53,22 @@ The installer:
43
53
  2. Backs it up in the same directory with a timestamped suffix.
44
54
  3. Writes a new manifest pointing to this project's CLI.
45
55
  4. Records restore metadata in `~/.codex-control-chrome-mcp/install-state.json`.
56
+ 5. Records the genuine OpenAI host path for proxy mode when available.
57
+
58
+ ### Identifying the official host
59
+
60
+ The installer identifies OpenAI's genuine host with stable signals instead of the
61
+ binary filename (OpenAI has renamed it across releases, e.g. `extension-host` →
62
+ `Codex for Chrome`):
63
+
64
+ - the manifest `name` (`com.openai.codexextension`) and the official extension origin,
65
+ - the bundle directory structure (`.../openai-bundled/.../extension-host/...`),
66
+ - the binary format (a compiled Mach-O/ELF executable, not a shell script),
67
+ - on macOS, the OpenAI code signature (Developer ID Team `2DC432GLL2`).
68
+
69
+ When choosing the proxy target it prefers the **live** manifest (most up to date) and
70
+ skips paths that no longer exist, so a renamed or removed host is never recorded as the
71
+ official target. `status` reports these signals under `classification`.
46
72
 
47
73
  Example manifest path on macOS:
48
74
 
@@ -50,6 +76,12 @@ Example manifest path on macOS:
50
76
  ~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.openai.codexextension.json
51
77
  ```
52
78
 
79
+ Example manifest path on Linux:
80
+
81
+ ```text
82
+ ~/.config/google-chrome/NativeMessagingHosts/com.openai.codexextension.json
83
+ ```
84
+
53
85
  ## MCP Client Config
54
86
 
55
87
  Use stdio mode from other Agent tools:
@@ -112,8 +144,40 @@ This prints:
112
144
 
113
145
  Chrome launches the native host only when the extension connects. If no bridge socket is visible, reload Chrome or the Codex extension after installing the manifest.
114
146
 
147
+ ### Auto re-register after Codex updates
148
+
149
+ A Codex App/extension update re-registers its own `com.openai.codexextension` manifest and reverts this project's proxy registration. To avoid a manual repair each time, the MCP stdio server re-asserts the manifest on startup when it detects it was reverted (only if it was installed before — it never bootstraps a fresh install silently). Logs go to stderr only.
150
+
151
+ Re-asserting the manifest takes effect on the extension's **next** Native Messaging connection, so reload the Codex Chrome Extension or restart Chrome once after a Codex update. Disable this behavior with:
152
+
153
+ ```bash
154
+ CODEX_CONTROL_CHROME_NO_AUTO_REGISTER=1
155
+ ```
156
+
115
157
  If the extension is missing, install the [Codex Chrome Extension](https://chromewebstore.google.com/detail/hehggadaopoacecdllhhajmbjkdcmajg) from Chrome Web Store and enable it in the target Chrome profile.
116
158
 
117
159
  Stopping the MCP stdio process does not uninstall the native host. Uninstall explicitly to restore the original manifest.
118
160
 
119
161
  The MCP stdio server uses the official `@modelcontextprotocol/sdk` package and must not write logs to stdout. Use stderr for diagnostics.
162
+
163
+ ## Troubleshooting
164
+
165
+ If `status` shows no sockets:
166
+
167
+ 1. Confirm Chrome is running with the profile where the extension is installed.
168
+ 2. Reload or disable/enable the Codex Chrome Extension.
169
+ 3. Run `npx -y codex-control-chrome-mcp@latest status`.
170
+ 4. Check `registered`. If it is `false` and `classification.kind` is `official-openai`, a Codex update reclaimed the manifest. The stdio server re-asserts it on next startup; reload the extension or restart Chrome to reconnect (or run `install-native-host` to repair immediately).
171
+ 5. When `registered` is `true`, the active manifest points to `~/.codex-control-chrome-mcp/native-host-launcher` (or your local launcher) and the extension just needs to connect.
172
+
173
+ If Codex App integration is affected, uninstall this project:
174
+
175
+ ```bash
176
+ npx -y codex-control-chrome-mcp@latest uninstall-native-host
177
+ ```
178
+
179
+ If the recorded backup is stale or manually changed, inspect the manifest and use `--force` only when you intentionally want to restore/remove the current project manifest.
180
+
181
+ ## Publishing
182
+
183
+ This package is published from GitHub Actions through npm Trusted Publishing. See [Release Process](./release.md).
@@ -0,0 +1,54 @@
1
+ # Release Process
2
+
3
+ Releases are published from GitHub Actions through npm Trusted Publishing.
4
+
5
+ ## Trusted Publishing Setup
6
+
7
+ Configure the npm package settings:
8
+
9
+ - Package: `codex-control-chrome-mcp`
10
+ - Publisher: GitHub Actions
11
+ - Organization or user: `iola1999`
12
+ - Repository: `codex-control-chrome-mcp`
13
+ - Workflow filename: `publish.yml`
14
+ - Environment name: leave blank unless you add a GitHub deployment environment
15
+ - Allowed action: `npm publish`
16
+
17
+ Trusted Publishing requires a GitHub-hosted runner, OIDC permission, npm CLI 11.5.1 or newer, and Node.js 22.14.0 or newer. The publish workflow uses Node.js 24 and installs the latest npm before publishing.
18
+
19
+ ## Publish A New Version
20
+
21
+ 1. Update `package.json` version.
22
+ 2. Update `CHANGELOG.md`.
23
+ 3. Run checks locally:
24
+
25
+ ```bash
26
+ npm run ci
27
+ ```
28
+
29
+ 4. Commit and push to `main`.
30
+ 5. Create and push a matching tag:
31
+
32
+ ```bash
33
+ git tag v1.0.1
34
+ git push origin v1.0.1
35
+ ```
36
+
37
+ The tag must match the package version exactly. For example, `package.json` version `1.0.1` must use tag `v1.0.1`.
38
+
39
+ ## What The Workflow Does
40
+
41
+ The `publish.yml` workflow:
42
+
43
+ 1. Installs dependencies with `npm ci`.
44
+ 2. Verifies the tag matches `package.json`.
45
+ 3. Runs `npm run ci`.
46
+ 4. Publishes with `npm publish`.
47
+
48
+ No long-lived `NPM_TOKEN` is required.
49
+
50
+ ## GitHub Release
51
+
52
+ After the npm publish workflow succeeds, create a GitHub Release from the same tag and copy the relevant section from `CHANGELOG.md` into the release notes.
53
+
54
+ Do not create a new GitHub Release for a tag if the npm publish failed.
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "codex-control-chrome-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server and native host bridge for controlling Chrome through the Codex Chrome extension.",
5
+ "license": "MIT",
5
6
  "type": "module",
6
7
  "bin": {
7
8
  "codex-control-chrome-mcp": "bin/codex-control-chrome-mcp.js"
@@ -32,13 +33,18 @@
32
33
  "src",
33
34
  "docs",
34
35
  "skills",
35
- "README.md"
36
+ "CHANGELOG.md",
37
+ "LICENSE",
38
+ "README.md",
39
+ "SECURITY.md"
36
40
  ],
37
41
  "scripts": {
38
42
  "start": "node ./bin/codex-control-chrome-mcp.js --stdio",
39
43
  "native-host": "node ./bin/codex-control-chrome-mcp.js --native-host",
40
44
  "status": "node ./bin/codex-control-chrome-mcp.js status",
41
- "check": "node --check ./bin/codex-control-chrome-mcp.js && node --check ./src/*.js"
45
+ "check": "node --check ./bin/codex-control-chrome-mcp.js && node --check ./src/*.js && node --check ./test/*.test.js",
46
+ "test": "node --test",
47
+ "ci": "npm run check && npm test && npm pack --dry-run"
42
48
  },
43
49
  "engines": {
44
50
  "node": ">=20"
@@ -0,0 +1,126 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { execFile } from "node:child_process";
4
+ import { APP_STATE_DIR, CODEX_EXTENSION_ID, CODEX_NATIVE_HOST_NAME } from "./constants.js";
5
+
6
+ // Description we stamp on manifests we own. Used as one (not the only) signal.
7
+ export const MANIFEST_DESCRIPTION = "Codex Control Chrome MCP native messaging host";
8
+
9
+ /** A manifest this project wrote (description marker, or path inside our state dir). */
10
+ export function isProjectManifest(manifest) {
11
+ if (!manifest || typeof manifest !== "object") return false;
12
+ if (manifest.description === MANIFEST_DESCRIPTION) return true;
13
+ return isOurLauncherPath(manifest.path);
14
+ }
15
+
16
+ /** True when a host path points at a launcher we generated under APP_STATE_DIR. */
17
+ export function isOurLauncherPath(hostPath) {
18
+ if (typeof hostPath !== "string" || hostPath.length === 0) return false;
19
+ const resolved = path.resolve(hostPath);
20
+ const stateRoot = path.resolve(APP_STATE_DIR) + path.sep;
21
+ return resolved === path.resolve(APP_STATE_DIR) || resolved.startsWith(stateRoot);
22
+ }
23
+
24
+ /**
25
+ * Positive identification of OpenAI's genuine Codex native-host *manifest*,
26
+ * using stable identifiers (host name + the official extension id) rather than
27
+ * the binary filename (which OpenAI has renamed across releases).
28
+ */
29
+ export function isOfficialCodexManifest(manifest) {
30
+ if (!manifest || typeof manifest !== "object") return false;
31
+ if (isProjectManifest(manifest)) return false;
32
+ if (manifest.name !== CODEX_NATIVE_HOST_NAME) return false;
33
+ const origins = Array.isArray(manifest.allowed_origins) ? manifest.allowed_origins : [];
34
+ return origins.some((origin) => originMatchesCodexExtension(origin));
35
+ }
36
+
37
+ function originMatchesCodexExtension(origin) {
38
+ return typeof origin === "string" && origin.includes(`chrome-extension://${CODEX_EXTENSION_ID}`);
39
+ }
40
+
41
+ /** Stable directory-structure signature of the bundled Codex host (filename-agnostic). */
42
+ export function hostPathLooksOfficial(hostPath) {
43
+ if (typeof hostPath !== "string" || hostPath.length === 0) return false;
44
+ const normalized = path.resolve(hostPath).split(path.sep).join("/").toLowerCase();
45
+ return normalized.includes("/openai-bundled/") && normalized.includes("/extension-host/");
46
+ }
47
+
48
+ const MAGIC = {
49
+ "7f454c46": "elf", // ELF
50
+ feedface: "macho", // Mach-O 32 LE
51
+ feedfacf: "macho", // Mach-O 64 LE
52
+ cefaedfe: "macho", // Mach-O 32 BE
53
+ cffaedfe: "macho", // Mach-O 64 BE
54
+ cafebabe: "macho", // universal (fat) — also Java .class, acceptable for a host binary
55
+ bebafeca: "macho",
56
+ };
57
+
58
+ /** Inspect the host file: does it exist, is it a compiled binary or a shell script? */
59
+ export async function inspectHostBinary(hostPath) {
60
+ if (typeof hostPath !== "string" || hostPath.length === 0) {
61
+ return { exists: false, kind: "missing", executable: false };
62
+ }
63
+ let handle;
64
+ try {
65
+ const stat = await fs.stat(hostPath);
66
+ if (!stat.isFile()) return { exists: true, kind: "unknown", executable: false };
67
+ handle = await fs.open(hostPath, "r");
68
+ const buf = Buffer.alloc(4);
69
+ const { bytesRead } = await handle.read(buf, 0, 4, 0);
70
+ const head = buf.subarray(0, bytesRead);
71
+ let kind = "unknown";
72
+ if (head.length >= 2 && head[0] === 0x23 && head[1] === 0x21) kind = "script"; // #!
73
+ else if (head.length === 4 && MAGIC[head.toString("hex")]) kind = MAGIC[head.toString("hex")];
74
+ else if (head.length >= 2 && head[0] === 0x4d && head[1] === 0x5a) kind = "pe"; // MZ
75
+ return { exists: true, kind, executable: (stat.mode & 0o111) !== 0 };
76
+ } catch {
77
+ return { exists: false, kind: "missing", executable: false };
78
+ } finally {
79
+ await handle?.close().catch(() => {});
80
+ }
81
+ }
82
+
83
+ /** Best-effort macOS code-signature check; the strongest OpenAI signal when available. */
84
+ export function verifyOpenAiCodesign(hostPath) {
85
+ if (process.platform !== "darwin" || typeof hostPath !== "string" || !hostPath) {
86
+ return Promise.resolve({ available: false });
87
+ }
88
+ return new Promise((resolve) => {
89
+ execFile("codesign", ["-dvvv", hostPath], { timeout: 4000 }, (error, stdout, stderr) => {
90
+ const out = `${stdout || ""}${stderr || ""}`; // codesign -d writes to stderr
91
+ if (!out) return resolve({ available: false, error: error?.message });
92
+ const teamId = (out.match(/TeamIdentifier=(\S+)/) || [])[1] || null;
93
+ const authority = (out.match(/Authority=([^\r\n]+)/) || [])[1] || null;
94
+ const openai = /openai/i.test(out);
95
+ resolve({ available: true, signed: Boolean(authority), openai, teamId, authority });
96
+ });
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Full classification of a (manifest, hostPath) pair against stable signals.
102
+ * `trustedOfficial` means: looks like OpenAI's real host and is safe to back up /
103
+ * proxy to. We require the manifest-level identity plus at least one host-level
104
+ * signal (bundle path, compiled binary, or an OpenAI code signature).
105
+ */
106
+ export async function classifyHost({ manifest, hostPath } = {}) {
107
+ const resolvedPath = hostPath ?? manifest?.path ?? null;
108
+ const ours = isProjectManifest(manifest) || isOurLauncherPath(resolvedPath);
109
+ const officialManifest = isOfficialCodexManifest(manifest);
110
+ const pathOfficial = hostPathLooksOfficial(resolvedPath);
111
+ const binary = await inspectHostBinary(resolvedPath);
112
+ const codesign = await verifyOpenAiCodesign(resolvedPath);
113
+
114
+ const hostSignal = pathOfficial || codesign.openai === true || binary.kind === "macho" || binary.kind === "elf";
115
+ const trustedOfficial = !ours && officialManifest && hostSignal;
116
+
117
+ return {
118
+ ours,
119
+ officialManifest,
120
+ pathOfficial,
121
+ binary,
122
+ codesign,
123
+ trustedOfficial,
124
+ kind: ours ? "ours" : trustedOfficial ? "official-openai" : "unknown",
125
+ };
126
+ }
package/src/installer.js CHANGED
@@ -8,19 +8,26 @@ import {
8
8
  INSTALL_STATE_PATH,
9
9
  nativeHostManifestPath,
10
10
  } from "./constants.js";
11
-
12
- const PACKAGE_NAME = "codex-control-chrome-mcp";
13
- const PACKAGE_VERSION = "1.0.0";
14
- const MANIFEST_DESCRIPTION = "Codex Control Chrome MCP native messaging host";
15
-
16
- export async function installNativeHost({ binPath, proxy = true } = {}) {
17
- const manifestPath = nativeHostManifestPath();
11
+ import {
12
+ MANIFEST_DESCRIPTION,
13
+ classifyHost,
14
+ inspectHostBinary,
15
+ isOfficialCodexManifest,
16
+ isOurLauncherPath,
17
+ isProjectManifest,
18
+ } from "./host-identity.js";
19
+ import { PACKAGE_NAME, PACKAGE_VERSION } from "./package-meta.js";
20
+
21
+ export async function installNativeHost({ binPath, proxy = true, paths = defaultInstallPaths() } = {}) {
22
+ const { manifestPath, appStateDir, configPath, installStatePath } = paths;
18
23
  await fs.mkdir(path.dirname(manifestPath), { recursive: true });
19
- await fs.mkdir(APP_STATE_DIR, { recursive: true });
24
+ await fs.mkdir(appStateDir, { recursive: true });
20
25
 
21
- const hostPath = binPath ? await createLocalLauncher(path.resolve(binPath)) : await createNpxLauncher();
26
+ const hostPath = binPath
27
+ ? await createLocalLauncher(path.resolve(binPath), appStateDir)
28
+ : await createNpxLauncher(appStateDir);
22
29
  const existing = await readJsonIfExists(manifestPath);
23
- const previousState = await readJsonIfExists(INSTALL_STATE_PATH);
30
+ const previousState = await readJsonIfExists(installStatePath);
24
31
  const previousBackup = previousState?.backupPath
25
32
  ? await readJsonIfExists(previousState.backupPath)
26
33
  : null;
@@ -33,23 +40,9 @@ export async function installNativeHost({ binPath, proxy = true } = {}) {
33
40
  backupPath = previousState.backupPath;
34
41
  }
35
42
 
36
- let officialHostPath = null;
37
- if (proxy && previousBackup?.path && !previousBackupIsOurs) {
38
- officialHostPath = previousBackup.path;
39
- } else if (
40
- proxy &&
41
- typeof previousState?.officialHostPath === "string" &&
42
- path.resolve(previousState.officialHostPath) !== path.resolve(hostPath)
43
- ) {
44
- officialHostPath = previousState.officialHostPath;
45
- } else if (
46
- proxy &&
47
- existing?.path &&
48
- !existingIsOurs &&
49
- path.resolve(existing.path) !== path.resolve(hostPath)
50
- ) {
51
- officialHostPath = existing.path;
52
- }
43
+ const officialHostPath = proxy
44
+ ? await resolveOfficialHostPath({ existing, existingIsOurs, previousState, previousBackup, hostPath })
45
+ : null;
53
46
 
54
47
  const manifest = {
55
48
  allowed_origins: [`chrome-extension://${CODEX_EXTENSION_ID}/`],
@@ -64,19 +57,96 @@ export async function installNativeHost({ binPath, proxy = true } = {}) {
64
57
  installedAt: new Date().toISOString(),
65
58
  manifestPath,
66
59
  hostPath,
60
+ binPath: binPath ? path.resolve(binPath) : null,
67
61
  launchMode: binPath ? "direct-bin" : "npx-launcher",
68
62
  backupPath,
69
63
  officialHostPath,
70
64
  proxy,
71
65
  };
72
- await fs.writeFile(APP_CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf8");
73
- await fs.writeFile(INSTALL_STATE_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf8");
66
+ await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
67
+ await fs.writeFile(installStatePath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
74
68
  return config;
75
69
  }
76
70
 
77
- export async function uninstallNativeHost({ force = false } = {}) {
78
- const manifestPath = nativeHostManifestPath();
79
- const state = await readJsonIfExists(INSTALL_STATE_PATH);
71
+ /**
72
+ * Resolve the genuine OpenAI host to proxy to. Authority order:
73
+ * 1. the live manifest, when it is verifiably OpenAI's (most up to date),
74
+ * 2. the previously recorded official host,
75
+ * 3. the backed-up manifest's path.
76
+ * Self-references and our own launcher are dropped, and a candidate whose file
77
+ * still exists is preferred so a renamed/removed host (e.g. after a Codex
78
+ * update) never gets recorded as the official target.
79
+ */
80
+ async function resolveOfficialHostPath({ existing, existingIsOurs, previousState, previousBackup, hostPath }) {
81
+ const candidates = [];
82
+ if (existing?.path && !existingIsOurs && isOfficialCodexManifest(existing)) {
83
+ candidates.push(existing.path);
84
+ }
85
+ if (typeof previousState?.officialHostPath === "string") {
86
+ candidates.push(previousState.officialHostPath);
87
+ }
88
+ if (previousBackup?.path && !isProjectManifest(previousBackup)) {
89
+ candidates.push(previousBackup.path);
90
+ }
91
+
92
+ const seen = new Set();
93
+ const unique = [];
94
+ for (const candidate of candidates) {
95
+ const resolved = path.resolve(candidate);
96
+ if (resolved === path.resolve(hostPath) || isOurLauncherPath(candidate) || seen.has(resolved)) {
97
+ continue;
98
+ }
99
+ seen.add(resolved);
100
+ unique.push(candidate);
101
+ }
102
+
103
+ for (const candidate of unique) {
104
+ if ((await inspectHostBinary(candidate)).exists) return candidate;
105
+ }
106
+ // None of the candidates exist on disk; fall back to the most authoritative
107
+ // one (the live official manifest) for diagnostics rather than inventing null.
108
+ return unique[0] ?? null;
109
+ }
110
+
111
+ /**
112
+ * Idempotently re-assert a previously-established install. Used on stdio
113
+ * startup so a Codex update that reclaims the manifest self-heals without a
114
+ * manual `install-native-host`. Never bootstraps a fresh install silently.
115
+ */
116
+ export async function ensureNativeHostRegistered({ paths = defaultInstallPaths() } = {}) {
117
+ const { manifestPath, installStatePath } = paths;
118
+ const state = await readJsonIfExists(installStatePath);
119
+ if (!state || typeof state.hostPath !== "string") {
120
+ return { action: "skip", reason: "not-installed" };
121
+ }
122
+
123
+ const manifest = await readJsonIfExists(manifestPath);
124
+ const pointsToUs =
125
+ isProjectManifest(manifest) &&
126
+ typeof manifest?.path === "string" &&
127
+ path.resolve(manifest.path) === path.resolve(state.hostPath);
128
+ const launcherExists = (await inspectHostBinary(state.hostPath)).exists;
129
+ if (pointsToUs && launcherExists) {
130
+ return { action: "ok", reason: "already-registered" };
131
+ }
132
+
133
+ const result = await installNativeHost({
134
+ binPath: state.launchMode === "direct-bin" ? state.binPath ?? undefined : undefined,
135
+ proxy: state.proxy !== false,
136
+ paths,
137
+ });
138
+ return {
139
+ action: "re-registered",
140
+ reason: launcherExists ? "manifest-reverted" : "launcher-missing",
141
+ manifestPath,
142
+ hostPath: result.hostPath,
143
+ officialHostPath: result.officialHostPath,
144
+ };
145
+ }
146
+
147
+ export async function uninstallNativeHost({ force = false, paths = defaultInstallPaths() } = {}) {
148
+ const { manifestPath, installStatePath } = paths;
149
+ const state = await readJsonIfExists(installStatePath);
80
150
  const manifest = await readJsonIfExists(manifestPath);
81
151
 
82
152
  if (!manifest && !state?.backupPath) {
@@ -99,10 +169,25 @@ export async function uninstallNativeHost({ force = false } = {}) {
99
169
  }
100
170
 
101
171
  export async function nativeHostInstallStatus() {
102
- const manifestPath = nativeHostManifestPath();
172
+ const { manifestPath, installStatePath } = defaultInstallPaths();
103
173
  const manifest = await readJsonIfExists(manifestPath);
104
- const state = await readJsonIfExists(INSTALL_STATE_PATH);
105
- return { manifestPath, manifest, state };
174
+ const state = await readJsonIfExists(installStatePath);
175
+ const classification = await classifyHost({ manifest, hostPath: manifest?.path });
176
+ const registered =
177
+ isProjectManifest(manifest) &&
178
+ typeof state?.hostPath === "string" &&
179
+ typeof manifest?.path === "string" &&
180
+ path.resolve(manifest.path) === path.resolve(state.hostPath);
181
+ return { manifestPath, manifest, state, registered, classification };
182
+ }
183
+
184
+ function defaultInstallPaths() {
185
+ return {
186
+ manifestPath: nativeHostManifestPath(),
187
+ appStateDir: APP_STATE_DIR,
188
+ configPath: APP_CONFIG_PATH,
189
+ installStatePath: INSTALL_STATE_PATH,
190
+ };
106
191
  }
107
192
 
108
193
  async function backupManifest(manifestPath) {
@@ -115,11 +200,11 @@ async function backupManifest(manifestPath) {
115
200
  return backupPath;
116
201
  }
117
202
 
118
- async function createNpxLauncher() {
203
+ async function createNpxLauncher(appStateDir) {
119
204
  if (process.platform === "win32") {
120
205
  throw new Error("NPX native host launcher installation is not implemented for Windows yet.");
121
206
  }
122
- const launcherPath = path.join(APP_STATE_DIR, "native-host-launcher");
207
+ const launcherPath = path.join(appStateDir, "native-host-launcher");
123
208
  const script = `#!/usr/bin/env sh
124
209
  exec npx -y ${PACKAGE_NAME}@${PACKAGE_VERSION} --native-host "$@"
125
210
  `;
@@ -128,18 +213,14 @@ exec npx -y ${PACKAGE_NAME}@${PACKAGE_VERSION} --native-host "$@"
128
213
  return launcherPath;
129
214
  }
130
215
 
131
- function isProjectManifest(manifest) {
132
- return manifest?.description === MANIFEST_DESCRIPTION;
133
- }
134
-
135
- async function createLocalLauncher(binPath) {
216
+ async function createLocalLauncher(binPath, appStateDir) {
136
217
  if (process.platform === "win32") {
137
218
  return binPath;
138
219
  }
139
220
  if (!binPath.endsWith(".js")) {
140
221
  return binPath;
141
222
  }
142
- const launcherPath = path.join(APP_STATE_DIR, "native-host-local-launcher");
223
+ const launcherPath = path.join(appStateDir, "native-host-local-launcher");
143
224
  const script = `#!/usr/bin/env sh
144
225
  exec ${shellQuote(process.execPath)} ${shellQuote(binPath)} "$@"
145
226
  `;
package/src/mcp-server.js CHANGED
@@ -4,12 +4,15 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { z } from "zod";
5
5
  import { connectBridge, listBridgeSockets } from "./bridge-client.js";
6
6
  import { nativeHostManifestPath } from "./constants.js";
7
+ import { ensureNativeHostRegistered } from "./installer.js";
8
+ import { PACKAGE_NAME, PACKAGE_VERSION } from "./package-meta.js";
7
9
 
8
10
  export async function runMcpServer() {
11
+ await autoRegisterNativeHost();
9
12
  const runtime = new ChromeUseRuntime();
10
13
  const server = new McpServer({
11
- name: "codex-control-chrome-mcp",
12
- version: "1.0.0",
14
+ name: PACKAGE_NAME,
15
+ version: PACKAGE_VERSION,
13
16
  });
14
17
 
15
18
  registerTools(server, runtime);
@@ -18,6 +21,32 @@ export async function runMcpServer() {
18
21
  await server.connect(transport);
19
22
  }
20
23
 
24
+ /**
25
+ * On stdio startup, re-assert our native-host manifest if a Codex update (or
26
+ * anything else) reverted it. Best-effort and silent: it only logs to stderr
27
+ * (stdout is the MCP JSON-RPC channel) and never blocks server startup.
28
+ * Opt out with CODEX_CONTROL_CHROME_NO_AUTO_REGISTER=1.
29
+ *
30
+ * Note: re-asserting the manifest only takes effect on the Chrome extension's
31
+ * next Native Messaging connection (reload the extension or restart Chrome).
32
+ */
33
+ async function autoRegisterNativeHost() {
34
+ if (process.env.CODEX_CONTROL_CHROME_NO_AUTO_REGISTER === "1") return;
35
+ try {
36
+ const result = await ensureNativeHostRegistered();
37
+ if (result.action === "re-registered") {
38
+ process.stderr.write(
39
+ `[codex-control-chrome-mcp] native host manifest was ${result.reason}; re-registered it. ` +
40
+ "Reload the Codex Chrome extension (or restart Chrome) to reconnect the bridge.\n",
41
+ );
42
+ }
43
+ } catch (error) {
44
+ process.stderr.write(
45
+ `[codex-control-chrome-mcp] auto-register skipped: ${error?.message ?? error}\n`,
46
+ );
47
+ }
48
+ }
49
+
21
50
  class ChromeUseRuntime {
22
51
  constructor() {
23
52
  this.sessionId = `codex-control-chrome-mcp-${crypto.randomUUID()}`;
@@ -0,0 +1,6 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
4
+
5
+ export const PACKAGE_NAME = packageJson.name;
6
+ export const PACKAGE_VERSION = packageJson.version;