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: