Skip to content

Commit 06221a7

Browse files
committed
Expose Codex Apps as ordinary HTTP MCP servers
1 parent 8ce931a commit 06221a7

248 files changed

Lines changed: 26450 additions & 10841 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#!/usr/bin/env python3
2+
3+
"""Keep Codex Apps knowledge out of core and the generic MCP runtime."""
4+
5+
from __future__ import annotations
6+
7+
import re
8+
import sys
9+
import tomllib
10+
from pathlib import Path
11+
12+
13+
ROOT = Path(__file__).resolve().parents[2]
14+
PROTECTED_CRATES = (
15+
ROOT / "codex-rs" / "core",
16+
ROOT / "codex-rs" / "codex-mcp",
17+
)
18+
FORBIDDEN_PACKAGES = ("codex-apps", "codex-connectors")
19+
FORBIDDEN_SOURCE_PATTERNS = (
20+
re.compile(r"(?:\b|_)codex[ _-]?apps", re.IGNORECASE),
21+
re.compile(r"(?:\b|_)codex[ _-]?connectors?", re.IGNORECASE),
22+
re.compile(r"\bconnectors?\b", re.IGNORECASE),
23+
re.compile(r"\bconnector_[a-zA-Z0-9_]+\b"),
24+
re.compile(r"\bConnector[A-Z][a-zA-Z0-9_]*\b"),
25+
)
26+
HTTP_APPS_ROOTS = (
27+
ROOT / "codex-rs" / "apps" / "src",
28+
ROOT / "codex-rs" / "ext" / "mcp" / "src" / "apps",
29+
)
30+
FORBIDDEN_IN_PROCESS_PATTERN = re.compile(r"\b(?:InProcess|in_process)\b")
31+
32+
33+
def main() -> int:
34+
failures = []
35+
failures.extend(manifest_failures())
36+
failures.extend(source_failures())
37+
failures.extend(in_process_failures())
38+
39+
if not failures:
40+
return 0
41+
42+
print(
43+
"Codex Apps must remain ordinary HTTP MCP servers outside core and codex-mcp."
44+
)
45+
print(
46+
"Keep product behavior in codex-apps and its host extension; core and the generic "
47+
"MCP runtime may consume only ordinary MCP registrations and runtime metadata."
48+
)
49+
print()
50+
for failure in failures:
51+
print(f"- {failure}")
52+
53+
return 1
54+
55+
56+
def manifest_failures() -> list[str]:
57+
failures = []
58+
for crate_root in PROTECTED_CRATES:
59+
manifest_path = crate_root / "Cargo.toml"
60+
manifest = tomllib.loads(manifest_path.read_text())
61+
for section_name, dependencies in dependency_sections(manifest):
62+
for dependency_name, dependency in dependencies.items():
63+
package = (
64+
dependency.get("package", dependency_name)
65+
if isinstance(dependency, dict)
66+
else dependency_name
67+
)
68+
if package in FORBIDDEN_PACKAGES:
69+
failures.append(
70+
f"{relative_path(manifest_path)} declares `{package}` "
71+
f"in `[{section_name}]`"
72+
)
73+
return failures
74+
75+
76+
def dependency_sections(manifest: dict) -> list[tuple[str, dict]]:
77+
sections: list[tuple[str, dict]] = []
78+
for section_name in ("dependencies", "dev-dependencies", "build-dependencies"):
79+
dependencies = manifest.get(section_name)
80+
if isinstance(dependencies, dict):
81+
sections.append((section_name, dependencies))
82+
83+
for target_name, target in manifest.get("target", {}).items():
84+
if not isinstance(target, dict):
85+
continue
86+
for section_name in ("dependencies", "dev-dependencies", "build-dependencies"):
87+
dependencies = target.get(section_name)
88+
if isinstance(dependencies, dict):
89+
sections.append((f"target.{target_name}.{section_name}", dependencies))
90+
91+
return sections
92+
93+
94+
def source_failures() -> list[str]:
95+
failures = []
96+
for crate_root in PROTECTED_CRATES:
97+
for path in sorted((crate_root / "src").glob("**/*.rs")):
98+
for line_number, line in enumerate(path.read_text().splitlines(), start=1):
99+
if any(pattern.search(line) for pattern in FORBIDDEN_SOURCE_PATTERNS):
100+
failures.append(
101+
f"{relative_path(path)}:{line_number} contains Apps product knowledge"
102+
)
103+
return failures
104+
105+
106+
def in_process_failures() -> list[str]:
107+
failures = []
108+
for source_root in HTTP_APPS_ROOTS:
109+
for path in sorted(source_root.glob("**/*.rs")):
110+
for line_number, line in enumerate(path.read_text().splitlines(), start=1):
111+
if FORBIDDEN_IN_PROCESS_PATTERN.search(line):
112+
failures.append(
113+
f"{relative_path(path)}:{line_number} introduces an in-process Apps path"
114+
)
115+
return failures
116+
117+
118+
def relative_path(path: Path) -> str:
119+
return str(path.relative_to(ROOT))
120+
121+
122+
if __name__ == "__main__":
123+
sys.exit(main())

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ jobs:
2323
- name: Verify codex-tui does not import codex-core directly
2424
run: python3 .github/scripts/verify_tui_core_boundary.py
2525

26+
- name: Verify Codex Apps stays outside core and the generic MCP runtime
27+
run: python3 .github/scripts/verify_codex_apps_mcp_boundary.py
28+
2629
- name: Verify Bazel clippy flags match Cargo workspace lints
2730
run: python3 .github/scripts/verify_bazel_clippy_lints.py
2831

codex-rs/Cargo.lock

Lines changed: 64 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[workspace]
22
members = [
3+
"apps",
34
"aws-auth",
45
"analytics",
56
"agent-graph-store",
@@ -143,6 +144,7 @@ codex-agent-graph-store = { path = "agent-graph-store" }
143144
codex-agent-identity = { path = "agent-identity" }
144145
codex-ansi-escape = { path = "ansi-escape" }
145146
codex-api = { path = "codex-api" }
147+
codex-apps = { path = "apps" }
146148
codex-aws-auth = { path = "aws-auth" }
147149
codex-app-server = { path = "app-server" }
148150
codex-app-server-transport = { path = "app-server-transport" }

codex-rs/analytics/src/events.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use codex_login::default_client::originator;
3030
use codex_plugin::PluginId;
3131
use codex_plugin::PluginTelemetryMetadata;
3232
use codex_protocol::approvals::NetworkApprovalProtocol;
33+
use codex_protocol::mcp_approval_meta::McpToolSource;
3334
use codex_protocol::models::AdditionalPermissionProfile;
3435
use codex_protocol::models::SandboxPermissions;
3536
use codex_protocol::protocol::GuardianAssessmentOutcome;
@@ -260,6 +261,23 @@ pub enum GuardianReviewedAction {
260261
RequestPermissions {},
261262
}
262263

264+
impl GuardianReviewedAction {
265+
pub fn mcp_tool_call(
266+
server: String,
267+
tool_name: String,
268+
tool_title: Option<String>,
269+
source: Option<&McpToolSource>,
270+
) -> Self {
271+
Self::McpToolCall {
272+
server,
273+
tool_name,
274+
connector_id: source.map(McpToolSource::id).map(str::to_string),
275+
connector_name: source.map(McpToolSource::name).map(str::to_string),
276+
tool_title,
277+
}
278+
}
279+
}
280+
263281
#[derive(Clone, Serialize)]
264282
pub struct GuardianReviewEventParams {
265283
pub thread_id: String,

codex-rs/app-server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ axum = { workspace = true, default-features = false, features = [
3030
"ws",
3131
] }
3232
codex-analytics = { workspace = true }
33+
codex-apps = { workspace = true }
3334
codex-arg0 = { workspace = true }
3435
codex-cloud-config = { workspace = true }
3536
codex-config = { workspace = true }

0 commit comments

Comments
 (0)