Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions .github/scripts/verify_codex_apps_mcp_boundary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/usr/bin/env python3

"""Keep Codex Apps knowledge out of core, generic MCP, and generic host wiring."""

from __future__ import annotations

import re
import sys
import tomllib
from pathlib import Path


ROOT = Path(__file__).resolve().parents[2]
PROTECTED_CRATES = (
ROOT / "codex-rs" / "core",
ROOT / "codex-rs" / "codex-mcp",
ROOT / "codex-rs" / "mcp-server",
)
FORBIDDEN_PACKAGES = ("codex-apps", "codex-connectors")
FORBIDDEN_SOURCE_PATTERNS = (
re.compile(r"(?:\b|_)codex[ _-]?apps", re.IGNORECASE),
re.compile(r"(?:\b|_)codex[ _-]?connectors?", re.IGNORECASE),
re.compile(r"\bconnectors?\b", re.IGNORECASE),
re.compile(r"\bconnector_[a-zA-Z0-9_]+\b"),
re.compile(r"\bConnector[A-Z][a-zA-Z0-9_]*\b"),
)
HTTP_APPS_ROOTS = (
ROOT / "codex-rs" / "apps" / "src",
ROOT / "codex-rs" / "ext" / "mcp" / "src" / "apps",
)
FORBIDDEN_IN_PROCESS_PATTERN = re.compile(r"\b(?:InProcess|in_process)\b")


def main() -> int:
failures = []
failures.extend(manifest_failures())
failures.extend(source_failures())
failures.extend(in_process_failures())

if not failures:
return 0

print(
"Codex Apps must remain ordinary HTTP MCP servers outside core, codex-mcp, "
"and codex-mcp-server host wiring."
)
print(
"Keep product behavior in codex-apps and its host extension; core and the generic "
"MCP runtime may consume only ordinary MCP registrations and runtime metadata, "
"while generic hosts may compose opaque extension bundles."
)
print()
for failure in failures:
print(f"- {failure}")

return 1


def manifest_failures() -> list[str]:
failures = []
for crate_root in PROTECTED_CRATES:
manifest_path = crate_root / "Cargo.toml"
manifest = tomllib.loads(manifest_path.read_text())
for section_name, dependencies in dependency_sections(manifest):
for dependency_name, dependency in dependencies.items():
package = (
dependency.get("package", dependency_name)
if isinstance(dependency, dict)
else dependency_name
)
if package in FORBIDDEN_PACKAGES:
failures.append(
f"{relative_path(manifest_path)} declares `{package}` "
f"in `[{section_name}]`"
)
return failures


def dependency_sections(manifest: dict) -> list[tuple[str, dict]]:
sections: list[tuple[str, dict]] = []
for section_name in ("dependencies", "dev-dependencies", "build-dependencies"):
dependencies = manifest.get(section_name)
if isinstance(dependencies, dict):
sections.append((section_name, dependencies))

for target_name, target in manifest.get("target", {}).items():
if not isinstance(target, dict):
continue
for section_name in ("dependencies", "dev-dependencies", "build-dependencies"):
dependencies = target.get(section_name)
if isinstance(dependencies, dict):
sections.append((f"target.{target_name}.{section_name}", dependencies))

return sections


def source_failures() -> list[str]:
failures = []
for crate_root in PROTECTED_CRATES:
for path in sorted((crate_root / "src").glob("**/*.rs")):
for line_number, line in enumerate(path.read_text().splitlines(), start=1):
if any(pattern.search(line) for pattern in FORBIDDEN_SOURCE_PATTERNS):
failures.append(
f"{relative_path(path)}:{line_number} contains Apps product knowledge"
)
return failures


def in_process_failures() -> list[str]:
failures = []
for source_root in HTTP_APPS_ROOTS:
for path in sorted(source_root.glob("**/*.rs")):
for line_number, line in enumerate(path.read_text().splitlines(), start=1):
if FORBIDDEN_IN_PROCESS_PATTERN.search(line):
failures.append(
f"{relative_path(path)}:{line_number} introduces an in-process Apps path"
)
return failures


def relative_path(path: Path) -> str:
return str(path.relative_to(ROOT))


if __name__ == "__main__":
sys.exit(main())
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ jobs:
- name: Verify codex-tui does not import codex-core directly
run: python3 .github/scripts/verify_tui_core_boundary.py

- name: Verify Codex Apps stays outside core and the generic MCP runtime
run: python3 .github/scripts/verify_codex_apps_mcp_boundary.py

- name: Verify Bazel clippy flags match Cargo workspace lints
run: python3 .github/scripts/verify_bazel_clippy_lints.py

Expand Down
74 changes: 64 additions & 10 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions codex-rs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[workspace]
members = [
"apps",
"aws-auth",
"analytics",
"agent-graph-store",
Expand Down Expand Up @@ -143,6 +144,7 @@ codex-agent-graph-store = { path = "agent-graph-store" }
codex-agent-identity = { path = "agent-identity" }
codex-ansi-escape = { path = "ansi-escape" }
codex-api = { path = "codex-api" }
codex-apps = { path = "apps" }
codex-aws-auth = { path = "aws-auth" }
codex-app-server = { path = "app-server" }
codex-app-server-transport = { path = "app-server-transport" }
Expand Down
18 changes: 18 additions & 0 deletions codex-rs/analytics/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ use codex_login::default_client::originator;
use codex_plugin::PluginId;
use codex_plugin::PluginTelemetryMetadata;
use codex_protocol::approvals::NetworkApprovalProtocol;
use codex_protocol::mcp_approval_meta::McpToolSource;
use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::models::SandboxPermissions;
use codex_protocol::protocol::GuardianAssessmentOutcome;
Expand Down Expand Up @@ -260,6 +261,23 @@ pub enum GuardianReviewedAction {
RequestPermissions {},
}

impl GuardianReviewedAction {
pub fn mcp_tool_call(
server: String,
tool_name: String,
tool_title: Option<String>,
source: Option<&McpToolSource>,
) -> Self {
Self::McpToolCall {
server,
tool_name,
connector_id: source.map(McpToolSource::id).map(str::to_string),
connector_name: source.map(McpToolSource::name).map(str::to_string),
tool_title,
}
}
}

#[derive(Clone, Serialize)]
pub struct GuardianReviewEventParams {
pub thread_id: String,
Expand Down
Loading
Loading