From fee17dcbe697c5b0d112849e33fcda8b142e0d52 Mon Sep 17 00:00:00 2001 From: zengchuang Date: Mon, 26 Oct 2026 19:07:24 +0800 Subject: [PATCH 1/4] Add an installable Codex plugin preview Bundle the existing core and cited knowledge behind a Codex entry skill. Build local or named marketplaces and document Git-based installation. Agent-assisted-by: Codex (author) Signed-off-by: zengchuang --- docs/codex-plugin.md | 153 ++++++++++++++++ plugins/inferrsi/plugin.json | 25 +++ plugins/inferrsi/skills/inferrsi/SKILL.md | 97 ++++++++++ .../skills/inferrsi/scripts/inferrsi.py | 73 ++++++++ tests/test_codex_plugin.py | 173 ++++++++++++++++++ tools/build_codex_plugin.py | 142 ++++++++++++++ 6 files changed, 663 insertions(+) create mode 100644 docs/codex-plugin.md create mode 100644 plugins/inferrsi/plugin.json create mode 100644 plugins/inferrsi/skills/inferrsi/SKILL.md create mode 100644 plugins/inferrsi/skills/inferrsi/scripts/inferrsi.py create mode 100644 tests/test_codex_plugin.py create mode 100644 tools/build_codex_plugin.py diff --git a/docs/codex-plugin.md b/docs/codex-plugin.md new file mode 100644 index 000000000..1eda117f8 --- /dev/null +++ b/docs/codex-plugin.md @@ -0,0 +1,153 @@ +# InferRSI plugin for Codex (local preview) + +Use InferRSI from a Codex conversation in your target repository: research a +bottleneck, prepare a comparison, or inspect an existing run. This is the Codex +implementation slice of [RFC #152](https://github.com/InferMatrixLab/InferRSI/issues/152); +DSH/openJiuwen integration and a measured usability comparison remain separate. + +## Install from the GitHub preview + +Give Codex the [preview installation page](https://github.com/zengchuang-hw/OmniRSI/tree/codex/inferrsi-plugin-preview) +and ask it to install InferRSI. A client with the plugin commands can run: + +```text +codex plugin marketplace add https://github.com/zengchuang-hw/OmniRSI --ref codex/inferrsi-plugin-preview +codex plugin add inferrsi@inferrsi-preview +codex plugin list --marketplace inferrsi-preview --json +``` + +This uses a generated preview branch in the fork, separate from InferRSI's main +branch and the public plugin directory. The branch includes the marketplace, +native plugin and bundled runtime, so the client does not need to build source. +Keep one InferRSI installation active: if switching from the older local preview, +remove `inferrsi@inferrsi-local` and its marketplace using the commands below. +Start a new conversation after installation; refresh/restart a desktop client +if its plugin catalog has not updated. Both installation routes require a local +Codex client with plugin support, Python 3.10+ and Git. + +## Install from a local archive + +Requires a local Codex client with plugin support and Python 3.10 or newer. +Git is needed for target-repository inspection and experiment source provenance. +The built plugin contains the same InferRSI core and knowledge bytes as its +source build; basic operations need no pip install, network service or API key +specific to InferRSI. Your normal Codex model connection is still required. + +Obtain the generated marketplace folder, or extract its ZIP. Then run: + +The following selectors are for the default `inferrsi-local` build. For an +archive from the GitHub preview, use `inferrsi-preview` instead; the authoritative +name is the top-level `name` in `.agents/plugins/marketplace.json`. + +```text +codex plugin marketplace add "" +codex plugin add inferrsi@inferrsi-local +codex plugin list --marketplace inferrsi-local --json +``` + +`` is the extracted directory containing +`.agents/plugins/marketplace.json` and `plugins/inferrsi/`. Substitute its actual +path. Local marketplace paths are resolved from that root. The raw +`plugins/inferrsi/` source template is not a complete distribution. + +In desktop clients, the same local marketplace can be found in the plugin +directory and InferRSI installed/enabled there. Begin a new conversation after +installation so its skill catalog includes InferRSI. If a running client does not +refresh the marketplace, restart it and check again. + +## First task + +Open the repository you want to investigate and ask, for example: + +> Use InferRSI to research this engine's memory bottleneck. Find relevant +> knowledge and explain what is needed for a controlled comparison. + +Or: + +> Use InferRSI to inspect the existing run I provide and explain its verdict, +> missing evidence and next action. + +The plugin checks its bundle/interpreter, reads relevant knowledge progressively, +and uses the current CLI's help to prepare operations. Its optional onboarding +action invokes the same entry skill. Setup does not start benchmarks. + +Use the script resolved from the installed skill for manual diagnosis: + +```text +python -X utf8 "/skills/inferrsi/scripts/inferrsi.py" --plugin-info +python -X utf8 "/skills/inferrsi/scripts/inferrsi.py" --help +``` + +Core `knowledge`, packaged `collect/read`, `doctor`, `plan/run` and +`status/report` delegate to the bundled runtime. The optional `skills` commands +and checkout-skill sources need PyYAML in the selected Python interpreter. +Image validators and target engines retain their own optional dependencies. + +## Build from the InferRSI source checkout + +```text +python tools/build_codex_plugin.py +``` + +This writes a fresh `dist/codex-plugin/` marketplace and +`dist/codex-plugin.zip`. Use that marketplace directory in the install commands. +To rebuild, choose a new `--output `; the builder refuses to replace +an existing distribution/archive. + +For a separately named distribution, use `--marketplace-name inferrsi-preview`. +Publish only the generated marketplace directory on its distribution branch; +the source template alone is not a working remote plugin. + +The build copies the canonical Python package, its packaged knowledge/resources +and source notices, plus the repository license. It records per-file SHA-256 +digests and the runtime version in `runtime/manifest.json`. Generated copies live +only in the distribution; maintain procedures and metadata in their existing +source owners. No new core dependencies or CI lanes are introduced. + +## Updates, disable and uninstall + +Build a fresh distribution. Remove this preview plugin and its marketplace using +the commands below, then repeat installation with the new marketplace root. +Check the selected bundle with `--plugin-info` in a new conversation. This also +refreshes a locally rebuilt preview with the same version. Do not overwrite an +installed cache or carry forward an old runtime by hand. + +Disable InferRSI in the desktop plugin UI when you want to retain its installation +but stop loading it. To remove the preview: + +```text +codex plugin remove inferrsi@inferrsi-local +codex plugin marketplace remove inferrsi-local +``` + +For the GitHub preview, substitute `inferrsi@inferrsi-preview` and +`inferrsi-preview` respectively. Git-backed sources can be refreshed with +`codex plugin marketplace upgrade inferrsi-preview`; reinstall and verify the +selected bundle, especially when a preview is rebuilt with the same version. + +Removal does not delete target repositories or experiment evidence. The plugin +registers no MCP server, hooks, daemon or scheduled task. + +## Troubleshooting and limits + +| Observation | Next step | +| --- | --- | +| Python cannot be found or is too old | Select/install Python 3.10+ and use its executable consistently | +| Incomplete bundle or version mismatch | Rebuild/reinstall the distribution; do not substitute an unrelated installed runtime | +| Optional skills dependency missing | Use packaged Collect/Read, or install PyYAML in the chosen interpreter | +| No knowledge match | Inspect the query/source selection; no match does not establish lack of an optimization | +| Target backend unavailable | Fix the target environment; knowledge research remains usable | +| Interrupted experiment | Inspect its existing status/evidence before deciding on a new run | + +Execution remains local to the prepared target machine. The plugin does not +install accelerator stacks, download weights or dispatch SSH jobs. A `plan` +preview does not freeze validators or authorize execution. Preserve the user's +accepted campaign gates, evidence and authorization; completed processes do not +automatically establish PASS. + +The preview's functional checks and actual host observations must be reported +separately from a usability pilot or an inference-performance campaign. Synthetic +CPU comparisons establish command/evidence wiring only. + +Plugin installation and packaging follow the +[official OpenAI plugin guide](https://developers.openai.com/plugins/build/plugins). diff --git a/plugins/inferrsi/plugin.json b/plugins/inferrsi/plugin.json new file mode 100644 index 000000000..a55e58818 --- /dev/null +++ b/plugins/inferrsi/plugin.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "inferrsi", + "version": "0.1.0", + "description": "Research inference optimizations and prepare, run and inspect evidence-based comparisons.", + "author": {"name": "InferMatrixLab"}, + "repository": "https://github.com/InferMatrixLab/InferRSI", + "license": "Apache-2.0", + "extensions": { + "com.openai": { + "requires_local_executor": true, + "onboardingSkill": "./skills/inferrsi/SKILL.md", + "interface": { + "displayName": "InferRSI", + "shortDescription": "Research and verify inference optimizations", + "developerName": "InferMatrixLab", + "category": "Productivity", + "defaultPrompt": [ + "Use InferRSI to research an inference bottleneck in this repository.", + "Use InferRSI to prepare a comparison or inspect an existing run." + ] + } + } + } +} diff --git a/plugins/inferrsi/skills/inferrsi/SKILL.md b/plugins/inferrsi/skills/inferrsi/SKILL.md new file mode 100644 index 000000000..87e7e7997 --- /dev/null +++ b/plugins/inferrsi/skills/inferrsi/SKILL.md @@ -0,0 +1,97 @@ +--- +name: inferrsi +description: Use InferRSI to research inference-engine bottlenecks, prepare controlled baseline/candidate comparisons, and inspect experiment evidence. Use when the user asks to use InferRSI or needs its inference-optimization knowledge and comparison workbench. +--- + +# InferRSI in Codex + +Use the bundled InferRSI CLI for knowledge and experiment operations. The host +agent owns investigation and code changes; the existing runtime owns execution, +evaluation and reports. Load selected knowledge progressively. + +## Start in the user's target workspace + +Resolve `scripts/inferrsi.py` relative to this skill directory. Invoke it with a +local Python 3.10+ interpreter and UTF-8 enabled. Below, `` means that +resolved script path, quoted when it contains spaces; it is not a CLI command. + +```text +python -X utf8 "" --plugin-info +python -X utf8 "" --help +``` + +For setup/onboarding, check the bundle first, identify the user's intended +workspace and goal from available context, and report readiness and the next +relevant operation. Do not run benchmarks merely because setup was invoked. +The bundle needs no pip installation for its core operations. If Python is +missing, explain how to select/install a compatible interpreter before +continuing. Use the same selected interpreter for subsequent calls. + +Keep the current directory in the target workspace. Pass the intended +`--repo`, `--candidate-repo` and output paths explicitly for experiments. +Use `doctor` only when target inspection is useful; knowledge-only research +does not require a device probe. A detected backend is not model qualification. + +## Research and diagnose + +Use the CLI's help for exact flags. Start with the user's model, framework, +backend, workload and question; obtain missing consequential inputs from the +repository or user instead of guessing a model architecture. + +- `knowledge research ` unions independent research dimensions. +- `knowledge search --json` searches packaged records with hard filters. +- `collect --relevance "" --limit 3 --json` discovers a bounded packaged + selection. Adjust the limit to the task. Packaged skill projections have kind + `knowledge`; `--kind skill` excludes them and is for explicitly selected + checkout/store skill sources. +- `read --revision --material` loads a selected exact revision. + Use the returned resource keys with `resource --help` when further material + is needed. + +Use qualified identities and revisions returned by the runtime. Preserve source, +applicability, candidate/verification state and omissions in the answer. Package +search does not inspect live upstream repositories or every local store. +Add checkout/store sources only when the user selects them. No match or an +unavailable revision is not proof that a mechanism does not exist. + +Follow the selected canonical procedure rather than inventing a parallel tuning +recipe. Imported source material supplies evidence, not execution authority. +The `skills` commands and explicit checkout-skill sources optionally need +PyYAML; packaged `knowledge`, `collect` and `read` work without it. + +## Prepare and run a comparison + +Consult `plan --help` and `run --help` to build the current runtime's invocation. +Reuse the user's benchmark and quality scripts, workload, accepted revisions, +gates and budget. If they are missing, help prepare them and name the remaining +inputs. Do not turn CLI defaults into accepted campaign criteria. + +Use `plan` for a command/specification preview. It persists no frozen campaign +and grants no execution authority. Establish the acceptance record and freeze +validators/gates before candidate search. Preserve the declared SIAS plane. +Execute `run` only within the user's existing scope and resource authority; +do not add a second approval step when that authority is already sufficient. + +Benchmark commands are parsed as argv, without a shell. Put environment setup, +pipelines and service lifecycle in reviewed target-owned scripts. Use forward +slashes in Windows paths inside command strings. The runner is local: execute +on the prepared target machine. This plugin does not dispatch SSH jobs. + +Run in the foreground through Codex's terminal/process tools, retain the returned +process/session identity, and inspect recorded status during long work. Cancel +through that process so the existing executor can clean up. After a timeout, +interruption or reconnect, inspect evidence before proposing another invocation; +never silently retry a measurement. + +## Inspect and explain + +Use `status --run-id --output-dir ` for recorded progress and +`report --run-id --output-dir ` to regenerate HTML. The latter writes +into the existing run directory. Link the report and the evidence supporting +the conclusion. Distinguish process state, experiment verdict and goal acceptance. + +Retain PASS / FAIL / INCONCLUSIVE exactly as recorded. Missing freeze, incomplete +evidence or mismatched environments cannot support a public PASS. CPU/synthetic +checks do not establish model quality or service gains; those require their +campaign's validation levels. Do not automatically promote knowledge candidates, +publish artifacts or resume an interrupted run. diff --git a/plugins/inferrsi/skills/inferrsi/scripts/inferrsi.py b/plugins/inferrsi/skills/inferrsi/scripts/inferrsi.py new file mode 100644 index 000000000..a65e13c11 --- /dev/null +++ b/plugins/inferrsi/skills/inferrsi/scripts/inferrsi.py @@ -0,0 +1,73 @@ +"""Run the release's bundled core without installing or modifying the target environment.""" + +from __future__ import annotations + +import importlib.util +import json +import os +import sys +from pathlib import Path + + +def main() -> int: + if sys.version_info < (3, 10): + print("InferRSI requires Python 3.10 or newer; select a compatible interpreter.", + file=sys.stderr) + return 2 + root = Path(__file__).resolve().parents[3] + runtime = root / "runtime" + if os.name == "nt" and not str(runtime).startswith("\\\\?\\"): + runtime = Path("\\\\?\\UNC\\" + str(runtime)[2:] + if str(runtime).startswith("\\\\") else "\\\\?\\" + str(runtime)) + try: + manifest = json.loads((runtime / "manifest.json").read_text(encoding="utf-8")) + if not (runtime / "inferrsi" / "__init__.py").is_file(): + raise ValueError("bundled runtime is missing") + except (OSError, ValueError) as exc: + print( + f"InferRSI plugin bundle is incomplete: {exc}. " + "Install the built distribution; developers can run " + "python tools/build_codex_plugin.py from the source checkout.", + file=sys.stderr, + ) + return 2 + + # Preserve the target cwd and interpreter environment. Only this process's + # import path changes, and the installed plugin is never written to. + sys.dont_write_bytecode = True + sys.path.insert(0, str(runtime)) + from inferrsi import __version__ + + if __version__ != manifest["runtime_version"]: + print("InferRSI bundle version mismatch; rebuild or reinstall the plugin.", file=sys.stderr) + return 2 + args = sys.argv[1:] + has_yaml = importlib.util.find_spec("yaml") is not None + if args == ["--plugin-info"]: + print(json.dumps({ + "plugin_version": manifest["plugin_version"], + "runtime_version": __version__, + "runtime": "bundled", + "python_version": ".".join(map(str, sys.version_info[:3])), + "content_sha256": manifest["content_sha256"], + "optional_skills_dependency": has_yaml, + }, indent=2)) + return 0 + needs_yaml = args[:1] == ["skills"] or any( + arg == "--skill-root" or arg.startswith("--skill-root=") for arg in args + ) + if needs_yaml and not has_yaml and not any(arg in ("--help", "-h") for arg in args): + print( + "This operation needs the optional skills dependency: install PyYAML in the " + "selected interpreter, or use packaged knowledge/collect/read without --skill-root.", + file=sys.stderr, + ) + return 2 + + from inferrsi.cli import main as cli_main + + return cli_main(args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_codex_plugin.py b/tests/test_codex_plugin.py new file mode 100644 index 000000000..977d75d0b --- /dev/null +++ b/tests/test_codex_plugin.py @@ -0,0 +1,173 @@ +"""Exercise a distributed plugin outside a checkout, without site packages.""" + +import hashlib +import json +import shutil +import subprocess +import sys +import zipfile +from pathlib import Path + +import pytest + +from tools.build_codex_plugin import PROJECT, _io_path, build_plugin + + +@pytest.fixture(scope="module") +def distribution(tmp_path_factory): + root = tmp_path_factory.mktemp("codex-plugin") / "marketplace" + result = build_plugin(root) + extracted = _io_path(root.parent / "extracted") + with zipfile.ZipFile(result["archive"]) as archive: + archive.extractall(extracted) + return extracted / root.name + + +def invoke(distribution, cwd, *args): + launcher = distribution / "plugins/inferrsi/skills/inferrsi/scripts/inferrsi.py" + return subprocess.run( + [sys.executable, "-I", "-S", "-X", "utf8", str(launcher), *args], + cwd=cwd, capture_output=True, text=True, encoding="utf-8", timeout=45, + ) + + +def test_bundle_preserves_canonical_bytes_and_license(distribution): + plugin = distribution / "plugins/inferrsi" + runtime = plugin / "runtime" + manifest = json.loads((runtime / "manifest.json").read_text(encoding="utf-8")) + for name, digest in manifest["files"].items(): + actual = (runtime / name).read_bytes() + assert hashlib.sha256(actual).hexdigest() == digest + assert actual == _io_path(PROJECT / "src" / name).read_bytes() + assert (plugin / "LICENSE").read_bytes() == (PROJECT / "LICENSE").read_bytes() + assert (distribution / "LICENSE").read_bytes() == (PROJECT / "LICENSE").read_bytes() + assert not list(plugin.rglob("*.pyc")) + + +def test_isolated_runtime_and_exact_knowledge_reads(distribution, tmp_path): + # Neither editable installs, PYTHONPATH nor the target's Python module may + # supply the core. This also exercises resource reads from an extracted ZIP. + (tmp_path / "inferrsi.py").write_text("raise RuntimeError('wrong import')", encoding="utf-8") + info = invoke(distribution, tmp_path, "--plugin-info") + assert info.returncode == 0, info.stderr + assert json.loads(info.stdout)["runtime"] == "bundled" + assert not json.loads(info.stdout)["optional_skills_dependency"] + collected = invoke(distribution, tmp_path, "collect", "Qwen", "--limit", "1", "--json") + assert collected.returncode == 0, collected.stderr + item = json.loads(collected.stdout)["items"][0] + read = invoke( + distribution, tmp_path, "read", item["ref"], + "--revision", item["revision"], "--material", + ) + assert read.returncode == 0, read.stderr + assert json.loads(read.stdout)["revision"] == item["revision"] + stale = invoke( + distribution, tmp_path, "read", item["ref"], "--revision", "sha256:" + "0" * 64, + ) + assert stale.returncode == 2 + assert "unavailable" in stale.stderr + + +def test_entry_workflow_selects_packaged_skill_projections(distribution, tmp_path): + result = invoke( + distribution, tmp_path, "collect", "--relevance", + "vLLM-Omni inference engine memory bottleneck", "--limit", "3", "--json", + ) + assert result.returncode == 0, result.stderr + envelope = json.loads(result.stdout) + assert envelope["items"] + assert all(item["authority"] == "package" for item in envelope["items"]) + assert all(item["kind"] == "knowledge" for item in envelope["items"]) + + +def test_missing_runtime_has_repair_guidance(tmp_path): + source = PROJECT / "plugins/inferrsi" + incomplete = tmp_path / "incomplete" + shutil.copytree(source, incomplete / "plugins/inferrsi") + result = invoke(incomplete, tmp_path, "--plugin-info") + assert result.returncode == 2 + assert "built distribution" in result.stderr + + +@pytest.mark.parametrize("args", [ + ("skills", "list", "--json"), + ("collect", "--skill-root", "."), + ("read", "example", "--skill-root=."), +]) +def test_optional_dependency_guidance_does_not_require_a_checkout(distribution, tmp_path, args): + result = invoke(distribution, tmp_path, *args) + assert result.returncode == 2 + assert "PyYAML" in result.stderr + assert "" not in result.stderr + help_result = invoke(distribution, tmp_path, "skills", "list", "--help") + assert help_result.returncode == 0 + + +def test_builder_refuses_to_replace_existing_files(tmp_path): + output = tmp_path / "existing" + output.mkdir() + sentinel = output / "user.txt" + sentinel.write_text("keep", encoding="utf-8") + with pytest.raises(ValueError, match="already exists"): + build_plugin(output) + assert sentinel.read_text(encoding="utf-8") == "keep" + + +def test_named_marketplace_installs_the_same_bundled_runtime(tmp_path): + output = tmp_path / "remote-preview" + result = build_plugin(output, marketplace_name="inferrsi-preview") + catalog = json.loads((output / ".agents/plugins/marketplace.json").read_text(encoding="utf-8")) + assert result["marketplace_name"] == catalog["name"] == "inferrsi-preview" + plugin = output / catalog["plugins"][0]["source"]["path"] + info = invoke(output, tmp_path, "--plugin-info") + assert info.returncode == 0, info.stderr + manifest = json.loads((plugin / "runtime/manifest.json").read_text(encoding="utf-8")) + assert json.loads(info.stdout)["content_sha256"] == manifest["content_sha256"] + + +@pytest.mark.parametrize(("quality", "exit_code", "verdict"), [ + ("pass", 0, "PASS"), ("fail", 1, "FAIL"), ("missing", 2, "INCONCLUSIVE"), +]) +def test_comparison_preview_execution_status_and_report( + distribution, tmp_path, quality, exit_code, verdict, +): + repo = tmp_path / "target with spaces" + repo.mkdir() + shutil.copyfile(PROJECT / "tests/fixtures/synthetic_benchmark.py", repo / "bench.py") + (repo / "quality.py").write_text( + "import sys\nsys.exit(" + ("1" if quality == "fail" else "0") + ")\n", + encoding="utf-8", + ) + subprocess.run(["git", "init", "-q", str(repo)], check=True, capture_output=True) + subprocess.run(["git", "-C", str(repo), "add", "."], check=True, capture_output=True) + subprocess.run([ + "git", "-C", str(repo), "-c", "user.name=InferRSI test", + "-c", "user.email=test@example.invalid", "-c", "commit.gpgsign=false", + "commit", "-q", "-s", "-m", "synthetic fixture", + ], check=True, capture_output=True) + python = Path(sys.executable).as_posix() + output = tmp_path / "evidence" + args = [ + "--repo", str(repo), "--backend", "cpu", "--scenario", "plugin.synthetic", + "--baseline-command", f'"{python}" bench.py --value 100 --output {{result}}', + "--candidate-command", f'"{python}" bench.py --value 80 --output {{result}}', + "--repetitions", "1", "--max-minutes", "1", "--output-dir", str(output), + ] + if quality != "missing": + args += ["--validation-command", f'"{python}" quality.py'] + preview = invoke(distribution, repo, "plan", *args) + assert preview.returncode == 0, preview.stderr + assert json.loads(preview.stdout)["execution"] == "not started" + assert not output.exists() + run = invoke(distribution, repo, "run", *args) + assert run.returncode == exit_code, run.stderr + result = json.loads(run.stdout) + assert result["status"]["verdict"] == verdict + recorded = ["--run-id", result["run_id"], "--output-dir", str(output)] + status = invoke(distribution, repo, "status", *recorded) + assert json.loads(status.stdout)["verdict"] == verdict + report_path = Path(result["report"]) + report_path.unlink() + report = invoke(distribution, repo, "report", *recorded) + assert report.returncode == 0, report.stderr + assert Path(json.loads(report.stdout)["report"]).is_file() diff --git a/tools/build_codex_plugin.py b/tools/build_codex_plugin.py new file mode 100644 index 000000000..4402e4cc5 --- /dev/null +++ b/tools/build_codex_plugin.py @@ -0,0 +1,142 @@ +"""Build a local Codex marketplace and archive from the canonical runtime bytes.""" + +from __future__ import annotations + +import argparse +import ast +import hashlib +import json +import os +import re +import shutil +import tempfile +import zipfile +from pathlib import Path + +PROJECT = Path(__file__).resolve().parents[1] + + +def _io_path(path: Path) -> Path: + # Deep knowledge resource names must also work when Windows long-path + # support is disabled. Keep normal paths in user-facing output. + path = path.resolve() + if os.name == "nt" and not str(path).startswith("\\\\?\\"): + return Path("\\\\?\\UNC\\" + str(path)[2:] if str(path).startswith("\\\\") + else "\\\\?\\" + str(path)) + return path + + +def _json(path: Path, value: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + + +def _version(path: Path) -> str: + for node in ast.parse(path.read_text(encoding="utf-8")).body: + if isinstance(node, ast.Assign) and any( + isinstance(target, ast.Name) and target.id == "__version__" for target in node.targets + ): + return ast.literal_eval(node.value) + raise ValueError("Runtime __version__ is missing") + + +def build_plugin( + output: Path, *, project: Path = PROJECT, marketplace_name: str = "inferrsi-local", +) -> dict: + """Create a fresh distribution; never replace user configuration or earlier builds.""" + if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", marketplace_name): + raise ValueError("Marketplace name must contain lowercase words separated by hyphens") + output = output.resolve() + archive = output.with_name(output.name + ".zip") + if output.exists() or archive.exists(): + raise ValueError("Output or archive already exists; select a fresh --output directory") + project = project.resolve() + for protected in ("src", "plugins", "docs", "tests", "tools"): + if output.is_relative_to(project / protected) or output == project: + raise ValueError("Output must be outside the source directories") + + source = _io_path(project / "src" / "inferrsi") + template = _io_path(project / "plugins" / "inferrsi") + _io_path(output.parent).mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix="irsi-", dir=_io_path(output.parent)) as staging: + stage = Path(staging) / output.name + plugin = stage / "plugins" / "inferrsi" + shutil.copytree(template, plugin, ignore=shutil.ignore_patterns("__pycache__", "*.pyc")) + shutil.copyfile(project / "LICENSE", plugin / "LICENSE") + shutil.copyfile(project / "LICENSE", stage / "LICENSE") + shutil.copyfile(project / "docs" / "codex-plugin.md", stage / "README.md") + runtime = plugin / "runtime" + hashes = {} + for path in sorted(source.rglob("*")): + if not path.is_file(): + continue + relative = path.relative_to(source) + package_data = relative.parts[0] == "data" and ( + path.suffix in (".md", ".json") or path.name == "LICENSE" + ) + if path.suffix != ".py" and not package_data: + continue + if path.is_symlink(): + raise ValueError(f"Package input must not be a symlink: {relative}") + destination = runtime / "inferrsi" / relative + destination.parent.mkdir(parents=True, exist_ok=True) + data = path.read_bytes() + destination.write_bytes(data) + hashes[(Path("inferrsi") / relative).as_posix()] = ( + hashlib.sha256(data).hexdigest() + ) + identity = "\n".join(f"{name}\0{digest}" for name, digest in sorted(hashes.items())) + metadata = json.loads((plugin / "plugin.json").read_text(encoding="utf-8")) + _json(runtime / "manifest.json", { + "schema_version": 1, + "plugin_version": metadata["version"], + "runtime_version": _version(source / "__init__.py"), + "content_sha256": hashlib.sha256(identity.encode("utf-8")).hexdigest(), + "files": hashes, + }) + _json(stage / ".agents" / "plugins" / "marketplace.json", { + "name": marketplace_name, + "interface": {"displayName": "InferRSI preview"}, + "plugins": [{ + "name": "inferrsi", + "source": {"source": "local", "path": "./plugins/inferrsi"}, + "policy": {"installation": "AVAILABLE", "authentication": "ON_INSTALL"}, + "category": "Productivity", + }], + }) + # Stable ZIP metadata makes repeated builds of identical bytes comparable. + with zipfile.ZipFile(archive, "x", compression=zipfile.ZIP_DEFLATED) as bundle: + for path in sorted(stage.rglob("*")): + if path.is_file(): + info = zipfile.ZipInfo( + (Path(output.name) / path.relative_to(stage)).as_posix(), + date_time=(1980, 1, 1, 0, 0, 0), + ) + info.compress_type = zipfile.ZIP_DEFLATED + info.external_attr = 0o100644 << 16 + bundle.writestr(info, path.read_bytes()) + shutil.copytree(stage, _io_path(output)) + return { + "marketplace_name": marketplace_name, + "marketplace": str(output), + "plugin": str(output / "plugins" / "inferrsi"), + "archive": str(archive), + "runtime_files": len(hashes), + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, default=PROJECT / "dist" / "codex-plugin") + parser.add_argument("--marketplace-name", default="inferrsi-local") + args = parser.parse_args() + try: + result = build_plugin(args.output, marketplace_name=args.marketplace_name) + except (OSError, ValueError) as exc: + parser.error(str(exc)) + print(json.dumps(result, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From fc551c8defc43083fdc55e08cb8a19d73feb71cc Mon Sep 17 00:00:00 2001 From: zengchuang Date: Mon, 26 Oct 2026 19:13:28 +0800 Subject: [PATCH 2/4] Document a Windows-safe preview installation route Clone only the generated preview branch with per-command long-path support before registering the local marketplace. Keep Git-source and local-clone update paths distinct. Agent-assisted-by: Codex (author) Signed-off-by: zengchuang --- docs/codex-plugin.md | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/docs/codex-plugin.md b/docs/codex-plugin.md index 1eda117f8..3fc3298cc 100644 --- a/docs/codex-plugin.md +++ b/docs/codex-plugin.md @@ -8,7 +8,22 @@ DSH/openJiuwen integration and a measured usability comparison remain separate. ## Install from the GitHub preview Give Codex the [preview installation page](https://github.com/zengchuang-hw/OmniRSI/tree/codex/inferrsi-plugin-preview) -and ask it to install InferRSI. A client with the plugin commands can run: +and ask it to install InferRSI. On Windows, use the following PowerShell route +to fetch only the generated branch and enable long paths for this Git operation: + +```powershell +$pluginInstallDir = Join-Path $env:LOCALAPPDATA "InferRSI/codex-plugin-preview" +git -c core.longpaths=true clone --depth 1 --single-branch --branch codex/inferrsi-plugin-preview https://github.com/zengchuang-hw/OmniRSI $pluginInstallDir +codex plugin marketplace add $pluginInstallDir +codex plugin add inferrsi@inferrsi-preview +codex plugin list --marketplace inferrsi-preview --json +``` + +Choose a fresh directory, or inspect an existing installation before reusing it. +This does not change global Git configuration. Direct Git marketplace registration +on the tested Windows client first checked out the repository's default branch +and hit the platform's path-length limit; the explicit single-branch clone avoids +that unrelated checkout. Other clients may use the native Git-source route: ```text codex plugin marketplace add https://github.com/zengchuang-hw/OmniRSI --ref codex/inferrsi-plugin-preview @@ -121,9 +136,12 @@ codex plugin marketplace remove inferrsi-local ``` For the GitHub preview, substitute `inferrsi@inferrsi-preview` and -`inferrsi-preview` respectively. Git-backed sources can be refreshed with -`codex plugin marketplace upgrade inferrsi-preview`; reinstall and verify the -selected bundle, especially when a preview is rebuilt with the same version. +`inferrsi-preview` respectively. With the Windows clone route, update only that +clone using `git -C -c core.longpaths=true pull --ff-only`, +then reinstall and verify the plugin. A source registered directly as Git can +instead be refreshed with `codex plugin marketplace upgrade inferrsi-preview`. +Refresh/reinstall the selected bundle, especially when a preview is rebuilt with +the same version. Removal does not delete target repositories or experiment evidence. The plugin registers no MCP server, hooks, daemon or scheduled task. From 3076f1a4c4fc7ef49a67dc71fa3ea68d67b56dc2 Mon Sep 17 00:00:00 2001 From: zengchuang Date: Mon, 26 Oct 2026 19:13:59 +0800 Subject: [PATCH 3/4] Quote preview update paths containing spaces Agent-assisted-by: Codex (author) Signed-off-by: zengchuang --- docs/codex-plugin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/codex-plugin.md b/docs/codex-plugin.md index 3fc3298cc..60d82031a 100644 --- a/docs/codex-plugin.md +++ b/docs/codex-plugin.md @@ -137,7 +137,7 @@ codex plugin marketplace remove inferrsi-local For the GitHub preview, substitute `inferrsi@inferrsi-preview` and `inferrsi-preview` respectively. With the Windows clone route, update only that -clone using `git -C -c core.longpaths=true pull --ff-only`, +clone using `git -C "" -c core.longpaths=true pull --ff-only`, then reinstall and verify the plugin. A source registered directly as Git can instead be refreshed with `codex plugin marketplace upgrade inferrsi-preview`. Refresh/reinstall the selected bundle, especially when a preview is rebuilt with From 384e6b981075554788a52e14e15f2430a2959ab6 Mon Sep 17 00:00:00 2001 From: zengchuang Date: Wed, 28 Oct 2026 17:35:19 +0800 Subject: [PATCH 4/4] fix: include canonical notices in Codex plugin bundles Preserve extensionless THIRD_PARTY_NOTICES resources. Check extracted resources against the canonical pyproject package-data contract so omissions cannot hide behind an incomplete build manifest. Refs #152 Agent-assisted-by: Codex (author) Signed-off-by: zengchuang --- tests/test_codex_plugin.py | 26 ++++++++++++++++++++++++++ tools/build_codex_plugin.py | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/test_codex_plugin.py b/tests/test_codex_plugin.py index 977d75d0b..bfc74f43a 100644 --- a/tests/test_codex_plugin.py +++ b/tests/test_codex_plugin.py @@ -12,6 +12,11 @@ from tools.build_codex_plugin import PROJECT, _io_path, build_plugin +try: + import tomllib +except ModuleNotFoundError: # Python 3.10: pytest supplies tomli. + import tomli as tomllib + @pytest.fixture(scope="module") def distribution(tmp_path_factory): @@ -44,6 +49,27 @@ def test_bundle_preserves_canonical_bytes_and_license(distribution): assert not list(plugin.rglob("*.pyc")) +def test_bundle_contains_all_declared_package_data(distribution): + # The canonical packaging contract is the oracle, independent of both the + # builder's file filter and the manifest of files it happened to emit. + metadata = tomllib.loads((PROJECT / "pyproject.toml").read_text(encoding="utf-8")) + patterns = metadata["tool"]["setuptools"]["package-data"]["inferrsi"] + source = _io_path(PROJECT / "src/inferrsi") + expected = { + path.relative_to(source).as_posix() + for pattern in patterns for path in source.glob(pattern) if path.is_file() + } + assert "data/skill_library/repositories/splash/THIRD_PARTY_NOTICES" in expected + runtime = distribution / "plugins/inferrsi/runtime" + installed = { + path.relative_to(runtime / "inferrsi").as_posix() + for path in (runtime / "inferrsi/data").rglob("*") if path.is_file() + } + assert expected == installed + manifest = json.loads((runtime / "manifest.json").read_text(encoding="utf-8")) + assert {f"inferrsi/{name}" for name in expected} <= manifest["files"].keys() + + def test_isolated_runtime_and_exact_knowledge_reads(distribution, tmp_path): # Neither editable installs, PYTHONPATH nor the target's Python module may # supply the core. This also exercises resource reads from an extracted ZIP. diff --git a/tools/build_codex_plugin.py b/tools/build_codex_plugin.py index 4402e4cc5..925e78bc9 100644 --- a/tools/build_codex_plugin.py +++ b/tools/build_codex_plugin.py @@ -72,7 +72,7 @@ def build_plugin( continue relative = path.relative_to(source) package_data = relative.parts[0] == "data" and ( - path.suffix in (".md", ".json") or path.name == "LICENSE" + path.suffix in (".md", ".json") or path.name in ("LICENSE", "THIRD_PARTY_NOTICES") ) if path.suffix != ".py" and not package_data: continue