Skip to content
Merged
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
214 changes: 214 additions & 0 deletions .codex-plugin/check_compatibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""Read-only package preflight; never probes hosts, grants access, or dispatches."""

from __future__ import annotations

import argparse
import json
import stat
import tomllib
from pathlib import Path

SCHEMA = "2718lab-devkit/package-compatibility-v1"
IDENTITY = (
"name",
"version",
"description",
"author",
"homepage",
"repository",
"license",
"keywords",
)
NAME = "2718lab-devkit"
MAX_BYTES = 262144


class PackageError(ValueError):
pass


def _pairs(items):
result = {}
for key, value in items:
if key in result:
raise PackageError("duplicate_json_key")
result[key] = value
return result


def _read(root: Path, relative: str) -> str:
path = root
for part in Path(relative).parts:
path = path / part
if path.is_symlink():
raise PackageError("symlink_not_allowed")
info = path.stat()
if not stat.S_ISREG(info.st_mode) or info.st_size > MAX_BYTES:
raise PackageError("file_type_or_size_invalid")
return path.read_text(encoding="utf-8")


def inspect_package(root: Path) -> dict:
"""Validate this package's dual manifests without invoking any command."""
checks = []
if root.is_symlink() or not root.is_dir():
return {
"schema": SCHEMA,
"ok": False,
"checks": [{"check": "root", "ok": False, "reason": "directory_required"}],
"execution_authorized": False,
}

def check(name, operation):
try:
operation()
checks.append({"check": name, "ok": True})
except (
OSError,
UnicodeError,
ValueError,
KeyError,
TypeError,
AttributeError,
) as exc:
reason = (
str(exc)
if isinstance(exc, PackageError)
else "invalid_or_missing_package_data"
)
checks.append({"check": name, "ok": False, "reason": reason})

data = {}
for label, relative in (
("portable_manifest", "plugin.json"),
("codex_manifest", ".codex-plugin/plugin.json"),
("portable_mcp", "mcp.json"),
("codex_mcp", ".mcp.json"),
):

def load(label=label, relative=relative):
value = json.loads(_read(root, relative), object_pairs_hook=_pairs)
if type(value) is not dict:
raise PackageError("object_required")
data[label] = value

check(label, load)

def identity():
portable, legacy = data["portable_manifest"], data["codex_manifest"]
allowed = {"$schema", *IDENTITY, "extensions"}
Comment on lines +99 to +100

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (broader_impact): The compatibility preflight accepts arbitrary extra fields in .codex-plugin/plugin.json, including legacy execution surfaces such as hooks, agents, or skills, because it compares only the selected identity fields and interface. A package can therefore receive an ok: true report while its retained Codex manifest has gained an unreviewed runtime surface.

Triggers: When a checked package contains a legacy manifest with the expected identity but additional execution-related fields.

Suggested fix: Validate the legacy manifest against its closed expected field set, or explicitly reject all legacy runtime-surface fields before reporting success.

Suggested change
portable, legacy = data["portable_manifest"], data["codex_manifest"]
allowed = {"$schema", *IDENTITY, "extensions"}
portable, legacy = data["portable_manifest"], data["codex_manifest"]
allowed = {"$schema", *IDENTITY, "extensions"}
legacy_allowed = {*IDENTITY, "mcpServers", "interface"}
if set(legacy) != legacy_allowed:
raise PackageError("legacy_manifest_fields_differ")

if (
set(portable) != allowed
or portable["$schema"]
!= "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json"
):
raise PackageError("portable_manifest_fields_differ")
if portable["name"] != NAME or any(
portable[key] != legacy[key] for key in IDENTITY
):
raise PackageError("manifest_identity_drift")
extension = portable["extensions"]
if extension != {"com.openai": {"interface": legacy["interface"]}}:
raise PackageError("openai_overlay_drift")
if legacy.get("mcpServers") != "./.mcp.json":
raise PackageError("legacy_mcp_pointer_drift")
version = tomllib.loads(_read(root, "mcp-tools/pyproject.toml"))["project"][
"version"
]
if version != portable["version"]:
raise PackageError("python_version_drift")

check("identity_and_overlay", identity)

def transports():
portable, legacy = data["portable_mcp"], data["codex_mcp"]
if (
set(portable) != {"$schema", "mcpServers"}
or portable["$schema"]
!= "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json"
):
raise PackageError("portable_mcp_fields_differ")
if set(portable["mcpServers"]) != {NAME} or set(legacy["mcpServers"]) != {NAME}:
raise PackageError("server_inventory_drift")
new, old = portable["mcpServers"][NAME], legacy["mcpServers"][NAME]
expected = {
"type": "stdio",
"command": "uv",
"args": ["run", "--locked", "--no-dev", "python", "server.py"],
"cwd": "./mcp-tools",
"env": {
"CODEX_DEVKIT_DATA_ROOT": "${PLUGIN_DATA}/runtime",
"UV_PROJECT_ENVIRONMENT": "${PLUGIN_DATA}/python",
"UV_CACHE_DIR": "${PLUGIN_DATA}/uv-cache",
"PYTHONDONTWRITEBYTECODE": "1",
},
}
if new != expected:
raise PackageError("portable_launch_contract_drift")
if (
old.get("command") != new["command"]
or old.get("args") != new["args"]
or old.get("cwd") != "mcp-tools"
):
raise PackageError("legacy_launch_contract_drift")
_read(root, "mcp-tools/server.py")
_read(root, "mcp-tools/uv.lock")

check("stdio_launch_equivalence", transports)

def skills():
folder = root / "skills"
if folder.is_symlink() or not folder.is_dir():
raise PackageError("skills_directory_missing")
entries = list(folder.iterdir())
if len(entries) > 32:
raise PackageError("skills_inventory_unbounded")
names = []
for path in entries:
if path.is_symlink():
raise PackageError("symlink_not_allowed")
if not path.is_dir():
continue
text = _read(root, "skills/" + path.name + "/SKILL.md")
if (
not text.startswith("---\n")
or "\nname: " + path.name + "\n" not in text.split("\n---", 1)[0] + "\n"
):
raise PackageError("skill_identity_drift")
names.append(path.name)
if not {"fast-lane-routing", "code-atlas", "workflow-design"}.issubset(names):
raise PackageError("required_manual_missing")

check("packaged_skill_identity", skills)
return {
"schema": SCHEMA,
"ok": all(item["ok"] for item in checks),
"checks": checks,
"execution_authorized": False,
"not_verified": [
"client_installation",
"host_environment_forwarding",
"private_broker_attestation",
"model_or_effort_availability",
"agent_dispatch",
],
"required_host_inputs": [
"durable_data_root",
"project_or_thread_scope",
"actual_dispatch_capabilities",
],
}


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--plugin-root", type=Path, required=True)
args = parser.parse_args()
result = inspect_package(args.plugin_root)
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0 if result["ok"] else 1


if __name__ == "__main__":
raise SystemExit(main())
4 changes: 4 additions & 0 deletions .codex-plugin/main-artifact-allowlist.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
".codex-plugin/plugin.json",
".mcp.json",
"LICENSE",
"plugin.json",
"mcp.json",
".codex-plugin/check_compatibility.py",
"docs/compatibility/codex-2026-09.md",
".codex-plugin/fastlane_todo_projection.py",
"mcp-tools/pyproject.toml",
"mcp-tools/server.py",
Expand Down
4 changes: 4 additions & 0 deletions .codex-plugin/marketplace-artifact-allowlist.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
".codex-plugin/plugin.json",
".mcp.json",
"LICENSE",
"plugin.json",
"mcp.json",
".codex-plugin/check_compatibility.py",
"docs/compatibility/codex-2026-09.md",
".codex-plugin/fastlane_todo_projection.py",
"mcp-tools/pyproject.toml",
"mcp-tools/server.py",
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "2718lab-devkit",
"version": "1.1.5",
"version": "1.2.0",
"description": "Local MCP server for developer workflow coordination, indexing, and evidence handling.",
"author": {
"name": "2718lab",
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ jobs:
shell: bash
run: |
python -m pip install --disable-pip-version-check pytest==9.1.1
python -m pytest -q mcp-tools/tests/test_primary_artifact.py
python -m pytest -q mcp-tools/tests/test_primary_artifact.py mcp-tools/tests/test_package_compatibility.py

- name: Build deterministic artifact
id: build
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ jobs:
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
git merge-base --is-ancestor "${GITHUB_SHA}" origin/main
test "${GITHUB_SHA}" = "$(git rev-parse origin/main)"
python .codex-plugin/check_compatibility.py --plugin-root .
RELEASE_TAG_STATE=new
if git ls-remote --exit-code --tags origin "refs/tags/${RELEASE_TAG}" >/dev/null 2>&1; then
git fetch --no-tags origin "refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}"
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ The project follows Keep a Changelog conventions. A maintainer dispatches the
repository release workflow from current `main`; it creates a new annotated tag
only after the CI and artifact checks pass.

## [1.2.0] — Current Codex packaging and compatibility preflight

- Add portable Agent Plugins 1.0 root manifests alongside the retained Codex compatibility package.
- Include both entry points and a read-only compatibility checker in deterministic lean and marketplace artifacts.
- Detect metadata/version/launch/manual drift without probing credentials, changing host settings or authorizing dispatch.
- Document current plugin discovery, file-backed image workflow boundaries and current-catalog model selection with primary sources.
- Preserve the 17-tool MCP surface, model-neutral planning, legacy replay and private-host fail-closed gates.

## [Unreleased]

## [1.1.5] - 2026-09-05
Expand Down
24 changes: 18 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
[简体中文](README.zh-CN.md)

# 2718lab DevKit — Codex + MCP v1.1.5
# 2718lab DevKit — Codex + MCP v1.2.0

[![version](https://img.shields.io/badge/version-v1.1.5-blue)](./.codex-plugin/plugin.json)
[![version](https://img.shields.io/badge/version-v1.2.0-blue)](./.codex-plugin/plugin.json)
[![license](https://img.shields.io/badge/license-AGPL--3.0-blue)](LICENSE)

2718lab DevKit is a Codex-first engineering toolkit: a local, stdio-only MCP
runtime for bounded project indexing, Atlas evidence, Relay lifecycle
coordination, and deterministic Fast Lane planning, plus a compact Skill bundle
of reference manuals. This repository carries the versioned v1.1.5 package.
of reference manuals. This repository carries the versioned v1.2.0 package.
The checked-in manifest and allowlist define the executable runtime surface;
the manual map, install, build, and verification sections below describe the
supported workflow.
Expand Down Expand Up @@ -49,6 +49,18 @@ and continue to fail closed.
> work requires a declared-child split that strictly reduces conflict, or is
> UNSPLITTABLE.

## What is new in 1.2.0

Portable Agent Plugins entry points now accompany the retained Codex manifests.
Run the read-only package preflight before installing an extracted artifact:

python .codex-plugin/check_compatibility.py --plugin-root <extracted-plugin>

The JSON report checks package consistency, not host permissions or model
availability. See [the September 2026 compatibility guide](docs/compatibility/codex-2026-09.md)
for current plugin packaging, dynamic model selection and actual host boundaries.
The 17-tool runtime and fail-closed execution contracts remain intact.

## What is shipped

- Project Index exposes opaque workspace registration, bounded snapshots,
Expand Down Expand Up @@ -185,7 +197,7 @@ source of record remains `main` and immutable release tags.

Maintainers build that snapshot with the dedicated marketplace allowlist:

python .codex-plugin/build_main_artifact.py --plugin-root . --allowlist .codex-plugin/marketplace-artifact-allowlist.json --output <artifact-output-dir>/2718lab-devkit-marketplace-v1.1.5.zip
python .codex-plugin/build_main_artifact.py --plugin-root . --allowlist .codex-plugin/marketplace-artifact-allowlist.json --output <artifact-output-dir>/2718lab-devkit-marketplace-v1.2.0.zip

## Install and run locally

Expand Down Expand Up @@ -232,7 +244,7 @@ handles or falls back to an unrelated local start.
The allowlisted builder creates a deterministic ZIP outside the plugin source
tree. Choose an output directory outside the source tree:

python .codex-plugin/build_main_artifact.py --plugin-root . --output <artifact-output-dir>/2718lab-devkit-v1.1.5.zip
python .codex-plugin/build_main_artifact.py --plugin-root . --output <artifact-output-dir>/2718lab-devkit-v1.2.0.zip

The artifact contains the manifest, .mcp.json, LICENSE, the locked Python
project, and the runtime files selected by
Expand Down Expand Up @@ -408,7 +420,7 @@ freeze a transient regression count.

## Version

This repository represents the versioned v1.1.5 package. Release notes are
This repository represents the versioned v1.2.0 package. Release notes are
in [CHANGELOG.md](CHANGELOG.md); build and install from the checked-in manifest,
artifact allowlist, and locked dependency set. A maintainer dispatches Release
from current `main`; it validates all declared gates, creates the annotated tag,
Expand Down
Loading
Loading