From 8812d75abb244724a99a3843de588abeb5b99409 Mon Sep 17 00:00:00 2001
From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com>
Date: Mon, 26 Oct 2026 17:49:25 -0400
Subject: [PATCH 01/17] fix(mirror): include Context Guard's hook runtime files
---
scripts/generate_plugins_json.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/scripts/generate_plugins_json.py b/scripts/generate_plugins_json.py
index 22990fb26..637feb3e0 100644
--- a/scripts/generate_plugins_json.py
+++ b/scripts/generate_plugins_json.py
@@ -59,6 +59,10 @@
"mturac/everything-openai-codex",
}
EXTRA_MIRROR_PATHS = {
+ # Context Guard declares Skills in its manifest; its lifecycle hooks,
+ # script entry points and state schema live at separate top-level paths.
+ # Mirror only these published runtime paths, not arbitrary source files.
+ "GreenLv/codex-context-guard": ("hooks", "scripts", "assets/state.schema.json"),
# Workflow MCP bootstrap loads the sibling control-plane runtime; its MCP
# App also reads assets/sidebar-icon.svg and Skills reference helper scripts.
"TohmaN233/codex-agents-workflow": ("control-plane", "scripts", "assets"),
From 3e43399211bd567d58c63111ad7e5d3a30f15c78 Mon Sep 17 00:00:00 2001
From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com>
Date: Mon, 26 Oct 2026 17:49:32 -0400
Subject: [PATCH 02/17] test(mirror): cover Context Guard hook runtime
selection
---
tests/test_generate_plugins_json.py | 32 ++++++++++++++++++++++++++++-
1 file changed, 31 insertions(+), 1 deletion(-)
diff --git a/tests/test_generate_plugins_json.py b/tests/test_generate_plugins_json.py
index 4e95a2284..cc9802695 100644
--- a/tests/test_generate_plugins_json.py
+++ b/tests/test_generate_plugins_json.py
@@ -2,7 +2,7 @@
import tempfile
import unittest
from unittest.mock import patch
-from pathlib import Path
+from pathlib import Path, PurePosixPath
SPEC = importlib.util.spec_from_file_location(
"generate_plugins_json", Path(__file__).resolve().parents[1] / "scripts/generate_plugins_json.py"
@@ -12,6 +12,36 @@
class GeneratedBundleCleanupTests(unittest.TestCase):
+ def test_context_guard_mirror_includes_undeclared_hook_runtime(self):
+ root = PurePosixPath("codex-context-guard-main")
+ names = {
+ "codex-context-guard-main/.codex-plugin/plugin.json",
+ "codex-context-guard-main/hooks/hooks.json",
+ "codex-context-guard-main/scripts/context_guard.py",
+ "codex-context-guard-main/scripts/manage_plugin.py",
+ "codex-context-guard-main/assets/icon.svg",
+ "codex-context-guard-main/assets/state.schema.json",
+ "codex-context-guard-main/internal/not-for-distribution.txt",
+ }
+ manifest = {
+ "name": "context-guard",
+ "skills": "./skills/",
+ "interface": {"composerIcon": "./assets/icon.svg"},
+ }
+ plugin = {"owner": "GreenLv", "repo": "codex-context-guard"}
+
+ paths = MODULE.collect_selected_paths(manifest, names, root, plugin)
+
+ self.assertTrue({
+ ".codex-plugin/plugin.json",
+ "hooks/hooks.json",
+ "scripts/context_guard.py",
+ "scripts/manage_plugin.py",
+ "assets/icon.svg",
+ "assets/state.schema.json",
+ }.issubset(paths))
+ self.assertNotIn("internal/not-for-distribution.txt", paths)
+
def test_branch_qualified_readme_entry_uses_source_branch_metadata(self):
with tempfile.TemporaryDirectory() as scratch:
readme = Path(scratch) / "README.md"
From 681aee2c6f5b880529bb0f0abd97f51be4731497 Mon Sep 17 00:00:00 2001
From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com>
Date: Tue, 27 Oct 2026 06:29:47 -0400
Subject: [PATCH 03/17] fix(mirror): exclude Context Guard's ignored cache and
build artifacts
---
scripts/generate_plugins_json.py | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/scripts/generate_plugins_json.py b/scripts/generate_plugins_json.py
index 637feb3e0..06e2f132b 100644
--- a/scripts/generate_plugins_json.py
+++ b/scripts/generate_plugins_json.py
@@ -323,6 +323,19 @@ def collect_selected_paths(
for extra_path in EXTRA_MIRROR_PATHS.get(f"{plugin['owner']}/{plugin['repo']}", ()):
add_recursive_selection(selected, all_names, plugin_root, extra_path)
+ # Context Guard's published .codexignore excludes transient build/cache
+ # artifacts. Never ship them merely because its hook runtime uses scripts/.
+ if f"{plugin['owner']}/{plugin['repo']}" == "GreenLv/codex-context-guard":
+ ignored_dirs = {
+ ".codex", ".git", ".venv", "__pycache__",
+ ".pytest_cache", ".ruff_cache", "build", "dist",
+ }
+ selected = {
+ path for path in selected
+ if not ignored_dirs.intersection(PurePosixPath(path).parts)
+ and not path.endswith(".pyc")
+ }
+
return selected
From b443dcecbc01fec6dbcee195b0cb34970ac2ca85 Mon Sep 17 00:00:00 2001
From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com>
Date: Tue, 27 Oct 2026 06:30:17 -0400
Subject: [PATCH 04/17] test(mirror): preserve Context Guard hook dependencies
without ignored files
---
tests/test_generate_plugins_json.py | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/tests/test_generate_plugins_json.py b/tests/test_generate_plugins_json.py
index cc9802695..31c56427d 100644
--- a/tests/test_generate_plugins_json.py
+++ b/tests/test_generate_plugins_json.py
@@ -19,6 +19,13 @@ def test_context_guard_mirror_includes_undeclared_hook_runtime(self):
"codex-context-guard-main/hooks/hooks.json",
"codex-context-guard-main/scripts/context_guard.py",
"codex-context-guard-main/scripts/manage_plugin.py",
+ "codex-context-guard-main/scripts/cg_instruction.py",
+ "codex-context-guard-main/scripts/run_context_guard.sh",
+ "codex-context-guard-main/scripts/run-context-guard.ps1",
+ "codex-context-guard-main/scripts/__pycache__/context_guard.cpython-314.pyc",
+ "codex-context-guard-main/scripts/build/temp.json",
+ "codex-context-guard-main/hooks/.pytest_cache/state",
+ "codex-context-guard-main/scripts/dist/bundle.js",
"codex-context-guard-main/assets/icon.svg",
"codex-context-guard-main/assets/state.schema.json",
"codex-context-guard-main/internal/not-for-distribution.txt",
@@ -37,10 +44,21 @@ def test_context_guard_mirror_includes_undeclared_hook_runtime(self):
"hooks/hooks.json",
"scripts/context_guard.py",
"scripts/manage_plugin.py",
+ "scripts/cg_instruction.py",
+ "scripts/run_context_guard.sh",
+ "scripts/run-context-guard.ps1",
"assets/icon.svg",
"assets/state.schema.json",
}.issubset(paths))
self.assertNotIn("internal/not-for-distribution.txt", paths)
+ self.assertTrue(all(
+ name not in paths for name in (
+ "scripts/__pycache__/context_guard.cpython-314.pyc",
+ "scripts/build/temp.json",
+ "hooks/.pytest_cache/state",
+ "scripts/dist/bundle.js",
+ )
+ ))
def test_branch_qualified_readme_entry_uses_source_branch_metadata(self):
with tempfile.TemporaryDirectory() as scratch:
From 5cf84cadbbbc51b5a0cb2e829bd4996e25e5eaf1 Mon Sep 17 00:00:00 2001
From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com>
Date: Tue, 27 Oct 2026 07:30:52 -0400
Subject: [PATCH 05/17] test(mirror): cover Context Guard's published install
smoke runner
---
tests/test_generate_plugins_json.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/tests/test_generate_plugins_json.py b/tests/test_generate_plugins_json.py
index 31c56427d..8017c9317 100644
--- a/tests/test_generate_plugins_json.py
+++ b/tests/test_generate_plugins_json.py
@@ -19,6 +19,7 @@ def test_context_guard_mirror_includes_undeclared_hook_runtime(self):
"codex-context-guard-main/hooks/hooks.json",
"codex-context-guard-main/scripts/context_guard.py",
"codex-context-guard-main/scripts/manage_plugin.py",
+ "codex-context-guard-main/scripts/smoke_installed.py",
"codex-context-guard-main/scripts/cg_instruction.py",
"codex-context-guard-main/scripts/run_context_guard.sh",
"codex-context-guard-main/scripts/run-context-guard.ps1",
@@ -44,6 +45,7 @@ def test_context_guard_mirror_includes_undeclared_hook_runtime(self):
"hooks/hooks.json",
"scripts/context_guard.py",
"scripts/manage_plugin.py",
+ "scripts/smoke_installed.py",
"scripts/cg_instruction.py",
"scripts/run_context_guard.sh",
"scripts/run-context-guard.ps1",
From 13844761f7e1785376a4c58d778c3243a251222e Mon Sep 17 00:00:00 2001
From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com>
Date: Tue, 27 Oct 2026 08:00:13 -0400
Subject: [PATCH 06/17] refactor(generator): extract output and retirement
helpers
---
scripts/plugin_output_utils.py | 49 ++++++++++++++++++++++++++++++++++
1 file changed, 49 insertions(+)
create mode 100644 scripts/plugin_output_utils.py
diff --git a/scripts/plugin_output_utils.py b/scripts/plugin_output_utils.py
new file mode 100644
index 000000000..a78d04dae
--- /dev/null
+++ b/scripts/plugin_output_utils.py
@@ -0,0 +1,49 @@
+"""JSON artifact writing and explicit retired bundle cleanup helpers.
+
+Kept separate from the marketplace generator to preserve the 500-line source limit.
+"""
+
+from pathlib import Path
+import json
+import shutil
+
+RETIRED_BUNDLE_IDENTITIES = {("samuelbushi", "uizze"), ("waynehamadi", "datacircle-plugin")}
+
+def write_json(path: Path, data: dict[str, object]) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
+
+
+def prune_unlisted_bundles(root: Path, plugins: list[dict[str, str]]) -> list[str]:
+ """Remove only explicitly reviewed retirements that are absent from README.
+
+ Omission from the parsed catalog never authorizes another retirement.
+ Keep listed repositories even if this run cannot fetch them, and preflight
+ the two-level tree for symlinks before deleting any approved bundle.
+ """
+ if not plugins:
+ raise ValueError("Refusing bundle cleanup from an empty parsed README; explicit maintainer retirement is required")
+ retained = {(plugin["owner"].casefold(), plugin["repo"].casefold()) for plugin in plugins}
+ removed: list[str] = []
+ if root.is_symlink():
+ raise ValueError("Generated plugins root must not be a symlink")
+ if not root.exists():
+ return removed
+ # Preflight the entire tree before retiring anything. A later invalid
+ # directory must not leave an earlier bundle partially retired.
+ retired: list[Path] = []
+ for owner in sorted(root.iterdir()):
+ if owner.is_symlink():
+ raise ValueError(f"Generated owner directory must not be a symlink: {owner}")
+ if not owner.is_dir():
+ continue
+ for repo in sorted(owner.iterdir()):
+ if repo.is_symlink():
+ raise ValueError(f"Generated repo directory must not be a symlink: {repo}")
+ identity = (owner.name.casefold(), repo.name.casefold())
+ if repo.is_dir() and identity in RETIRED_BUNDLE_IDENTITIES and identity not in retained:
+ retired.append(repo)
+ for repo in retired:
+ shutil.rmtree(repo)
+ removed.append(f"{repo.parent.name}/{repo.name}")
+ return removed
From 199f8a05c218dc1285dce2d4d47b506fcb87d97f Mon Sep 17 00:00:00 2001
From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com>
Date: Tue, 27 Oct 2026 08:00:16 -0400
Subject: [PATCH 07/17] refactor(generator): import extracted output helpers
---
scripts/generate_plugins_json.py | 44 +++-----------------------------
1 file changed, 4 insertions(+), 40 deletions(-)
diff --git a/scripts/generate_plugins_json.py b/scripts/generate_plugins_json.py
index 06e2f132b..4300eb5d6 100644
--- a/scripts/generate_plugins_json.py
+++ b/scripts/generate_plugins_json.py
@@ -24,11 +24,14 @@
from pathlib import Path, PurePosixPath
from urllib.parse import quote, unquote
+# Both direct script execution and importlib-based tests load this helper.
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+from plugin_output_utils import prune_unlisted_bundles, write_json
+
README = Path(__file__).parent.parent / "README.md"
OUTPUT = Path(__file__).parent.parent / "plugins.json"
MARKETPLACE_OUTPUT = Path(__file__).parent.parent / ".agents" / "plugins" / "marketplace.json"
PLUGINS_ROOT = Path(__file__).parent.parent / "plugins"
-RETIRED_BUNDLE_IDENTITIES = {("samuelbushi", "uizze"), ("waynehamadi", "datacircle-plugin")}
REQUEST_TIMEOUT_SECONDS = 60
MAX_RETRIES = 3
USER_AGENT = "awesome-codex-plugins-generator"
@@ -486,45 +489,6 @@ def build_marketplace_entry(
return entry
-def write_json(path: Path, data: dict[str, object]) -> None:
- path.parent.mkdir(parents=True, exist_ok=True)
- path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
-
-
-def prune_unlisted_bundles(root: Path, plugins: list[dict[str, str]]) -> list[str]:
- """Remove only explicitly reviewed retirements that are absent from README.
-
- Omission from the parsed catalog never authorizes another retirement.
- Keep listed repositories even if this run cannot fetch them, and preflight
- the two-level tree for symlinks before deleting any approved bundle.
- """
- if not plugins:
- raise ValueError("Refusing bundle cleanup from an empty parsed README; explicit maintainer retirement is required")
- retained = {(plugin["owner"].casefold(), plugin["repo"].casefold()) for plugin in plugins}
- removed: list[str] = []
- if root.is_symlink():
- raise ValueError("Generated plugins root must not be a symlink")
- if not root.exists():
- return removed
- # Preflight the entire tree before retiring anything. A later invalid
- # directory must not leave an earlier bundle partially retired.
- retired: list[Path] = []
- for owner in sorted(root.iterdir()):
- if owner.is_symlink():
- raise ValueError(f"Generated owner directory must not be a symlink: {owner}")
- if not owner.is_dir():
- continue
- for repo in sorted(owner.iterdir()):
- if repo.is_symlink():
- raise ValueError(f"Generated repo directory must not be a symlink: {repo}")
- identity = (owner.name.casefold(), repo.name.casefold())
- if repo.is_dir() and identity in RETIRED_BUNDLE_IDENTITIES and identity not in retained:
- retired.append(repo)
- for repo in retired:
- shutil.rmtree(repo)
- removed.append(f"{repo.parent.name}/{repo.name}")
- return removed
-
def main() -> None:
plugins = parse_plugins(README)
From e9dfd778d88102c3606c6752d006862b9c245d91 Mon Sep 17 00:00:00 2001
From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com>
Date: Tue, 27 Oct 2026 10:33:01 -0400
Subject: [PATCH 08/17] test(mirror): cover all Context Guard ignored runtime
artifacts
---
tests/test_generate_plugins_json.py | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/tests/test_generate_plugins_json.py b/tests/test_generate_plugins_json.py
index 8017c9317..edffc0271 100644
--- a/tests/test_generate_plugins_json.py
+++ b/tests/test_generate_plugins_json.py
@@ -27,6 +27,11 @@ def test_context_guard_mirror_includes_undeclared_hook_runtime(self):
"codex-context-guard-main/scripts/build/temp.json",
"codex-context-guard-main/hooks/.pytest_cache/state",
"codex-context-guard-main/scripts/dist/bundle.js",
+ "codex-context-guard-main/scripts/.venv/pyvenv.cfg",
+ "codex-context-guard-main/hooks/.ruff_cache/index",
+ "codex-context-guard-main/hooks/.codex/session.json",
+ "codex-context-guard-main/scripts/.git/config",
+ "codex-context-guard-main/scripts/compiled.pyc",
"codex-context-guard-main/assets/icon.svg",
"codex-context-guard-main/assets/state.schema.json",
"codex-context-guard-main/internal/not-for-distribution.txt",
@@ -59,6 +64,11 @@ def test_context_guard_mirror_includes_undeclared_hook_runtime(self):
"scripts/build/temp.json",
"hooks/.pytest_cache/state",
"scripts/dist/bundle.js",
+ "scripts/.venv/pyvenv.cfg",
+ "hooks/.ruff_cache/index",
+ "hooks/.codex/session.json",
+ "scripts/.git/config",
+ "scripts/compiled.pyc",
)
))
From d733211c48e1c085f4280b5d24f8cda77acbc36f Mon Sep 17 00:00:00 2001
From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com>
Date: Tue, 27 Oct 2026 11:32:05 -0400
Subject: [PATCH 09/17] fix(mirror): reject unsafe archive member paths before
bundle replacement
---
scripts/generate_plugins_json.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/scripts/generate_plugins_json.py b/scripts/generate_plugins_json.py
index 4300eb5d6..cf1f15f3d 100644
--- a/scripts/generate_plugins_json.py
+++ b/scripts/generate_plugins_json.py
@@ -424,6 +424,11 @@ def mirror_plugin_bundle(plugin: dict[str, str]) -> tuple[dict[str, object], str
if metadata_only
else collect_selected_paths(manifest, names, plugin_root, plugin)
)
+ # Refuse unsafe ZIP members before clearing an existing published bundle.
+ for relative_path in selected_paths:
+ path = PurePosixPath(relative_path)
+ if path.is_absolute() or ".." in path.parts or "\\" in relative_path:
+ raise ValueError(f"Unsafe mirrored archive path: {relative_path!r}")
mirrored_manifest = sanitize_metadata_only_manifest(manifest, plugin) if metadata_only else manifest
plugin_root_relative = plugin_root_relative_path(plugin_root)
From df620648bfd95ff7d3e4501b6c4ab9edeffa7cfe Mon Sep 17 00:00:00 2001
From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com>
Date: Tue, 27 Oct 2026 11:32:38 -0400
Subject: [PATCH 10/17] test(mirror): retain existing bundle on traversal
archive rejection
---
tests/test_generate_plugins_json.py | 31 +++++++++++++++++++++++++++++
1 file changed, 31 insertions(+)
diff --git a/tests/test_generate_plugins_json.py b/tests/test_generate_plugins_json.py
index edffc0271..a5f021097 100644
--- a/tests/test_generate_plugins_json.py
+++ b/tests/test_generate_plugins_json.py
@@ -72,6 +72,37 @@ def test_context_guard_mirror_includes_undeclared_hook_runtime(self):
)
))
+ def test_context_guard_rejects_archive_traversal_before_replacing_bundle(self):
+ import io
+ import zipfile
+
+ with tempfile.TemporaryDirectory() as scratch:
+ root = Path(scratch) / "plugins"
+ bundle = root / "GreenLv" / "codex-context-guard"
+ bundle.mkdir(parents=True)
+ sentinel = bundle / "keep.txt"
+ sentinel.write_text("preserve-existing-bundle", encoding="utf-8")
+
+ archive_bytes = io.BytesIO()
+ with zipfile.ZipFile(archive_bytes, "w") as source:
+ source.writestr(
+ "codex-context-guard-main/.codex-plugin/plugin.json",
+ '{"name":"context-guard","skills":"./skills/"}',
+ )
+ source.writestr("codex-context-guard-main/hooks/../../escape.txt", "unsafe")
+
+ archive_bytes.seek(0)
+ with zipfile.ZipFile(archive_bytes) as source, \
+ patch.object(MODULE, "fetch_repo_archive", return_value=source), \
+ patch.object(MODULE, "PLUGINS_ROOT", root):
+ with self.assertRaisesRegex(ValueError, "Unsafe mirrored archive path"):
+ MODULE.mirror_plugin_bundle(
+ {"owner": "GreenLv", "repo": "codex-context-guard"}
+ )
+
+ self.assertEqual(sentinel.read_text(encoding="utf-8"), "preserve-existing-bundle")
+ self.assertFalse((root / "escape.txt").exists())
+
def test_branch_qualified_readme_entry_uses_source_branch_metadata(self):
with tempfile.TemporaryDirectory() as scratch:
readme = Path(scratch) / "README.md"
From 77cfb7f200939aaf95fd71e5f45b34be9d8527f7 Mon Sep 17 00:00:00 2001
From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com>
Date: Tue, 27 Oct 2026 11:38:27 -0400
Subject: [PATCH 11/17] test(mirror): verify packaged Context Guard runtime
from synthetic archive
---
tests/test_generate_plugins_json.py | 49 +++++++++++++++++++++++++++++
1 file changed, 49 insertions(+)
diff --git a/tests/test_generate_plugins_json.py b/tests/test_generate_plugins_json.py
index a5f021097..7c29b03a9 100644
--- a/tests/test_generate_plugins_json.py
+++ b/tests/test_generate_plugins_json.py
@@ -103,6 +103,55 @@ def test_context_guard_rejects_archive_traversal_before_replacing_bundle(self):
self.assertEqual(sentinel.read_text(encoding="utf-8"), "preserve-existing-bundle")
self.assertFalse((root / "escape.txt").exists())
+ def test_context_guard_mirror_writes_install_runtime_from_archive(self):
+ import io
+ import json
+ import zipfile
+
+ with tempfile.TemporaryDirectory() as scratch:
+ root = Path(scratch) / "plugins"
+ source_bytes = io.BytesIO()
+ upstream_files = {
+ ".codex-plugin/plugin.json": json.dumps({
+ "name": "context-guard",
+ "skills": "./skills/",
+ "interface": {"composerIcon": "./assets/icon.svg"},
+ }),
+ "skills/guard/SKILL.md": "# Guard",
+ "hooks/hooks.json": '{"hooks":[]}',
+ "scripts/context_guard.py": "print('runtime')",
+ "scripts/manage_plugin.py": "print('manager')",
+ "scripts/smoke_installed.py": "print('smoke')",
+ "assets/state.schema.json": "{}",
+ "assets/icon.svg": "",
+ "scripts/__pycache__/cached.pyc": "not-a-bundle",
+ }
+ with zipfile.ZipFile(source_bytes, "w") as archive:
+ for path, value in upstream_files.items():
+ archive.writestr(f"codex-context-guard-main/{path}", value)
+
+ source_bytes.seek(0)
+ with zipfile.ZipFile(source_bytes) as archive, \
+ patch.object(MODULE, "fetch_repo_archive", return_value=archive), \
+ patch.object(MODULE, "PLUGINS_ROOT", root):
+ manifest, marketplace_path, relative_root = MODULE.mirror_plugin_bundle({
+ "owner": "GreenLv", "repo": "codex-context-guard"
+ })
+
+ mirrored = root / "GreenLv" / "codex-context-guard"
+ self.assertEqual(manifest["name"], "context-guard")
+ self.assertEqual(marketplace_path, "./plugins/GreenLv/codex-context-guard")
+ self.assertEqual(relative_root, "")
+ for required in (
+ ".codex-plugin/plugin.json", "skills/guard/SKILL.md",
+ "hooks/hooks.json", "scripts/context_guard.py",
+ "scripts/manage_plugin.py", "scripts/smoke_installed.py",
+ "assets/state.schema.json", "assets/icon.svg",
+ ):
+ with self.subTest(path=required):
+ self.assertTrue((mirrored / required).is_file())
+ self.assertFalse((mirrored / "scripts/__pycache__/cached.pyc").exists())
+
def test_branch_qualified_readme_entry_uses_source_branch_metadata(self):
with tempfile.TemporaryDirectory() as scratch:
readme = Path(scratch) / "README.md"
From 19a07e19ff5060dc61d28a90bf1e244f444b1e97 Mon Sep 17 00:00:00 2001
From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com>
Date: Tue, 27 Oct 2026 12:52:00 -0400
Subject: [PATCH 12/17] test(mirror): scope Context Guard runtime paths to
original owner
---
tests/test_generate_plugins_json.py | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/tests/test_generate_plugins_json.py b/tests/test_generate_plugins_json.py
index 7c29b03a9..d398d0f7e 100644
--- a/tests/test_generate_plugins_json.py
+++ b/tests/test_generate_plugins_json.py
@@ -72,6 +72,25 @@ def test_context_guard_mirror_includes_undeclared_hook_runtime(self):
)
))
+ def test_context_guard_runtime_mirror_does_not_apply_to_unrelated_owner(self):
+ # A repository with the same basename must not inherit GreenLv's
+ # source-specific runtime paths or copy unrelated scripts/hooks.
+ root = PurePosixPath("codex-context-guard-main")
+ names = {
+ "codex-context-guard-main/.codex-plugin/plugin.json",
+ "codex-context-guard-main/hooks/hooks.json",
+ "codex-context-guard-main/scripts/context_guard.py",
+ "codex-context-guard-main/assets/state.schema.json",
+ }
+ manifest = {"name": "context-guard", "skills": "./skills/"}
+
+ paths = MODULE.collect_selected_paths(
+ manifest, names, root,
+ {"owner": "unrelated-owner", "repo": "codex-context-guard"},
+ )
+
+ self.assertEqual(paths, {".codex-plugin/plugin.json"})
+
def test_context_guard_rejects_archive_traversal_before_replacing_bundle(self):
import io
import zipfile
From 5eea8bf98fdd03b387afa61da266e3e12bc2e794 Mon Sep 17 00:00:00 2001
From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com>
Date: Tue, 27 Oct 2026 14:32:01 -0400
Subject: [PATCH 13/17] fix(mirror): match Context Guard owner casing
consistently
---
scripts/generate_plugins_json.py | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/scripts/generate_plugins_json.py b/scripts/generate_plugins_json.py
index cf1f15f3d..5e394788f 100644
--- a/scripts/generate_plugins_json.py
+++ b/scripts/generate_plugins_json.py
@@ -323,12 +323,19 @@ def collect_selected_paths(
if isinstance(screenshot, str):
add_recursive_selection(selected, all_names, plugin_root, screenshot)
- for extra_path in EXTRA_MIRROR_PATHS.get(f"{plugin['owner']}/{plugin['repo']}", ()):
+ # GitHub repository names are case-insensitive. Match the canonical
+ # source identity even if its README link uses different capitalization.
+ repo_key = f"{plugin['owner']}/{plugin['repo']}".casefold()
+ extra_paths = next(
+ (paths for key, paths in EXTRA_MIRROR_PATHS.items() if key.casefold() == repo_key),
+ (),
+ )
+ for extra_path in extra_paths:
add_recursive_selection(selected, all_names, plugin_root, extra_path)
# Context Guard's published .codexignore excludes transient build/cache
# artifacts. Never ship them merely because its hook runtime uses scripts/.
- if f"{plugin['owner']}/{plugin['repo']}" == "GreenLv/codex-context-guard":
+ if repo_key == "greenlv/codex-context-guard":
ignored_dirs = {
".codex", ".git", ".venv", "__pycache__",
".pytest_cache", ".ruff_cache", "build", "dist",
From 55f1d0eba3851feb594b97e05c86ee239d8ca71c Mon Sep 17 00:00:00 2001
From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com>
Date: Tue, 27 Oct 2026 14:33:33 -0400
Subject: [PATCH 14/17] test(mirror): cover case-insensitive upstream
repository links
---
tests/test_generate_plugins_json.py | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/tests/test_generate_plugins_json.py b/tests/test_generate_plugins_json.py
index d398d0f7e..2545e0dce 100644
--- a/tests/test_generate_plugins_json.py
+++ b/tests/test_generate_plugins_json.py
@@ -72,6 +72,30 @@ def test_context_guard_mirror_includes_undeclared_hook_runtime(self):
)
))
+ def test_context_guard_runtime_mirror_accepts_github_link_case_variations(self):
+ # GitHub resolves owner/repository names case-insensitively. A README
+ # link with alternate casing must not omit the published hook runtime.
+ root = PurePosixPath("codex-context-guard-main")
+ names = {
+ "codex-context-guard-main/.codex-plugin/plugin.json",
+ "codex-context-guard-main/hooks/hooks.json",
+ "codex-context-guard-main/scripts/context_guard.py",
+ "codex-context-guard-main/scripts/compiled.pyc",
+ "codex-context-guard-main/assets/state.schema.json",
+ }
+ manifest = {"name": "context-guard", "skills": "./skills/"}
+ paths = MODULE.collect_selected_paths(
+ manifest, names, root,
+ {"owner": "greenlv", "repo": "CODEX-CONTEXT-GUARD"},
+ )
+ self.assertTrue({
+ ".codex-plugin/plugin.json",
+ "hooks/hooks.json",
+ "scripts/context_guard.py",
+ "assets/state.schema.json",
+ }.issubset(paths))
+ self.assertNotIn("scripts/compiled.pyc", paths)
+
def test_context_guard_runtime_mirror_does_not_apply_to_unrelated_owner(self):
# A repository with the same basename must not inherit GreenLv's
# source-specific runtime paths or copy unrelated scripts/hooks.
From 997294d2e34b3cb2552b83caa9b8b07d66cbd7f2 Mon Sep 17 00:00:00 2001
From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com>
Date: Tue, 27 Oct 2026 14:35:30 -0400
Subject: [PATCH 15/17] refactor(mirror): keep case-insensitive source
selection below line limit
---
scripts/generate_plugins_json.py | 10 +++-------
1 file changed, 3 insertions(+), 7 deletions(-)
diff --git a/scripts/generate_plugins_json.py b/scripts/generate_plugins_json.py
index 5e394788f..f64cee635 100644
--- a/scripts/generate_plugins_json.py
+++ b/scripts/generate_plugins_json.py
@@ -323,14 +323,10 @@ def collect_selected_paths(
if isinstance(screenshot, str):
add_recursive_selection(selected, all_names, plugin_root, screenshot)
- # GitHub repository names are case-insensitive. Match the canonical
- # source identity even if its README link uses different capitalization.
+ # GitHub resolves repository names regardless of capitalization.
repo_key = f"{plugin['owner']}/{plugin['repo']}".casefold()
- extra_paths = next(
- (paths for key, paths in EXTRA_MIRROR_PATHS.items() if key.casefold() == repo_key),
- (),
- )
- for extra_path in extra_paths:
+ source_paths = {key.casefold(): paths for key, paths in EXTRA_MIRROR_PATHS.items()}
+ for extra_path in source_paths.get(repo_key, ()):
add_recursive_selection(selected, all_names, plugin_root, extra_path)
# Context Guard's published .codexignore excludes transient build/cache
From b1ebf093402de670ef3678861d5b9aa5115fbe39 Mon Sep 17 00:00:00 2001
From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com>
Date: Tue, 27 Oct 2026 14:38:22 -0400
Subject: [PATCH 16/17] fix(mirror): include maintainer-identified Context
Guard startup assets
---
scripts/generate_plugins_json.py | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/scripts/generate_plugins_json.py b/scripts/generate_plugins_json.py
index f64cee635..11220c4be 100644
--- a/scripts/generate_plugins_json.py
+++ b/scripts/generate_plugins_json.py
@@ -62,10 +62,11 @@
"mturac/everything-openai-codex",
}
EXTRA_MIRROR_PATHS = {
- # Context Guard declares Skills in its manifest; its lifecycle hooks,
- # script entry points and state schema live at separate top-level paths.
- # Mirror only these published runtime paths, not arbitrary source files.
- "GreenLv/codex-context-guard": ("hooks", "scripts", "assets/state.schema.json"),
+ # Context Guard's hook runtime and startup assets are not declared as Skills.
+ "GreenLv/codex-context-guard": (
+ "hooks", "scripts", "assets/state.schema.json", "assets/core-intent-v2.json",
+ "assets/core-observation-v2.schema.json",
+ ),
# Workflow MCP bootstrap loads the sibling control-plane runtime; its MCP
# App also reads assets/sidebar-icon.svg and Skills reference helper scripts.
"TohmaN233/codex-agents-workflow": ("control-plane", "scripts", "assets"),
From c66453bcf675bb99ada38dc9f8336a44d6ffde68 Mon Sep 17 00:00:00 2001
From: Michael Kantor <6068672+kantorcodes@users.noreply.github.com>
Date: Tue, 27 Oct 2026 14:38:57 -0400
Subject: [PATCH 17/17] test(mirror): retain Context Guard startup inputs in
generated bundles
---
tests/test_generate_plugins_json.py | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/tests/test_generate_plugins_json.py b/tests/test_generate_plugins_json.py
index 2545e0dce..c882c0f0d 100644
--- a/tests/test_generate_plugins_json.py
+++ b/tests/test_generate_plugins_json.py
@@ -34,6 +34,8 @@ def test_context_guard_mirror_includes_undeclared_hook_runtime(self):
"codex-context-guard-main/scripts/compiled.pyc",
"codex-context-guard-main/assets/icon.svg",
"codex-context-guard-main/assets/state.schema.json",
+ "codex-context-guard-main/assets/core-intent-v2.json",
+ "codex-context-guard-main/assets/core-observation-v2.schema.json",
"codex-context-guard-main/internal/not-for-distribution.txt",
}
manifest = {
@@ -56,6 +58,8 @@ def test_context_guard_mirror_includes_undeclared_hook_runtime(self):
"scripts/run-context-guard.ps1",
"assets/icon.svg",
"assets/state.schema.json",
+ "assets/core-intent-v2.json",
+ "assets/core-observation-v2.schema.json",
}.issubset(paths))
self.assertNotIn("internal/not-for-distribution.txt", paths)
self.assertTrue(all(
@@ -82,6 +86,8 @@ def test_context_guard_runtime_mirror_accepts_github_link_case_variations(self):
"codex-context-guard-main/scripts/context_guard.py",
"codex-context-guard-main/scripts/compiled.pyc",
"codex-context-guard-main/assets/state.schema.json",
+ "codex-context-guard-main/assets/core-intent-v2.json",
+ "codex-context-guard-main/assets/core-observation-v2.schema.json",
}
manifest = {"name": "context-guard", "skills": "./skills/"}
paths = MODULE.collect_selected_paths(
@@ -93,6 +99,8 @@ def test_context_guard_runtime_mirror_accepts_github_link_case_variations(self):
"hooks/hooks.json",
"scripts/context_guard.py",
"assets/state.schema.json",
+ "assets/core-intent-v2.json",
+ "assets/core-observation-v2.schema.json",
}.issubset(paths))
self.assertNotIn("scripts/compiled.pyc", paths)
@@ -166,6 +174,8 @@ def test_context_guard_mirror_writes_install_runtime_from_archive(self):
"scripts/manage_plugin.py": "print('manager')",
"scripts/smoke_installed.py": "print('smoke')",
"assets/state.schema.json": "{}",
+ "assets/core-intent-v2.json": '{"schema":"core-intent-rules/v1","patterns":{}}',
+ "assets/core-observation-v2.schema.json": '{"$id":"urn:context-guard:core-observation:v2"}',
"assets/icon.svg": "",
"scripts/__pycache__/cached.pyc": "not-a-bundle",
}
@@ -190,10 +200,19 @@ def test_context_guard_mirror_writes_install_runtime_from_archive(self):
"hooks/hooks.json", "scripts/context_guard.py",
"scripts/manage_plugin.py", "scripts/smoke_installed.py",
"assets/state.schema.json", "assets/icon.svg",
+ "assets/core-intent-v2.json",
+ "assets/core-observation-v2.schema.json",
):
with self.subTest(path=required):
self.assertTrue((mirrored / required).is_file())
self.assertFalse((mirrored / "scripts/__pycache__/cached.pyc").exists())
+ # Bundle-level startup inputs are installed and parse as JSON.
+ intent = json.loads((mirrored / "assets/core-intent-v2.json").read_text())
+ observation = json.loads(
+ (mirrored / "assets/core-observation-v2.schema.json").read_text()
+ )
+ self.assertEqual(intent["schema"], "core-intent-rules/v1")
+ self.assertEqual(observation["$id"], "urn:context-guard:core-observation:v2")
def test_branch_qualified_readme_entry_uses_source_branch_metadata(self):
with tempfile.TemporaryDirectory() as scratch: