diff --git a/.github/workflows/notify-catalog.yml b/.github/workflows/notify-catalog.yml index f698f13..fb9405a 100644 --- a/.github/workflows/notify-catalog.yml +++ b/.github/workflows/notify-catalog.yml @@ -5,8 +5,8 @@ on: workflow_dispatch: workflow_call: secrets: - CATALOG_DISPATCH_TOKEN: - description: Existing receiver-scoped Actions write authority + CATALOG_APP_PRIVATE_KEY: + description: Private key of the receiver-scoped catalog GitHub App required: true outputs: run_url: @@ -23,6 +23,7 @@ jobs: steps: - name: Request complete catalog reconciliation id: catalog - uses: computer-mcp/computer-mcp.github.io/.github/actions/notify-catalog@fc27dd0f370d028a3e5021e3585274891f696578 + uses: computer-mcp/computer-mcp.github.io/.github/actions/notify-catalog@50298a84b08afdbc2642f4c765cce7c7dccf81e0 with: - token: ${{ secrets.CATALOG_DISPATCH_TOKEN }} + client-id: ${{ vars.CATALOG_APP_CLIENT_ID }} + private-key: ${{ secrets.CATALOG_APP_PRIVATE_KEY }} diff --git a/Documentation/Reference/Installation.md b/Documentation/Reference/Installation.md index 331d641..3ace6cb 100644 --- a/Documentation/Reference/Installation.md +++ b/Documentation/Reference/Installation.md @@ -3,7 +3,15 @@ On macOS the package owns `bin/codex-mcp-adapter` and its adjacent `codex-plugin_CodexAdapter.bundle`. On Windows it owns `bin/codex-mcp-adapter.exe`, `codex-plugin_CodexAdapter.resources` and the required -runtime DLLs in `bin/`. Codex itself remains an external dependency. +Swift/open-source runtime DLLs in `bin/`. Codex itself remains an external dependency. + +Before launching on Windows x86_64, install the latest supported +[Microsoft Visual C++ v14 x64 Redistributable](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170) +from Microsoft. The plugin does not include Microsoft runtime DLLs or install +the redistributable. Its release receipt records the minimum tested version for +each imported Microsoft DLL under `windows_runtime.external_prerequisites`. +If Windows reports a missing `VCRUNTIME140.dll`, `VCRUNTIME140_1.dll` or +`MSVCP140.dll`, install or update that official x64 package before retrying. Installing this package never installs, updates or removes Codex, changes global PATH, or grants a profile access to tools. @@ -38,7 +46,8 @@ builds SQLite from the checksummed source in `Scripts/windows-sqlite.json`, verifies its required features, and passes the resulting headers and static library to SwiftPM. It copies the recursively inspected Swift runtime DLLs beside the adapter and verifies their architecture and notice coverage. The -Windows receipt records each DLL's source digest and the SQLite build identity. +Windows receipt records each bundled DLL's source digest, the imported Microsoft +runtime version floors and the SQLite build identity. See [third-party components](../../THIRD_PARTY_NOTICES.md) for distribution terms. Package inputs must be regular files and directories. Symbolic links and special @@ -53,7 +62,9 @@ and manual dispatch. It checks formatting, tests and the dependency lock, then retains both native ZIPs and receipts as downloadable workflow artifacts. A separate Windows job installs no Swift toolchain, relocates the exact ZIP and runs the adapter with system-only child PATH. It records actual loaded module -paths and digests, MCP discovery, reconnect and joined native process cleanup. +paths and digests, verifies the system-installed Microsoft runtime architecture +and versions against the receipt, and checks MCP discovery, reconnect and joined +native process cleanup. Hosted runners can contain preinstalled software; this gate does not claim a pristine Windows installation or authenticated model execution. These outputs are validation builds, not published or verified official releases. diff --git a/README.md b/README.md index b15418e..7ef4cb6 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,8 @@ On macOS, building requires macOS 14 or newer and Swift 6.2 or newer. Run `swift build` and `swift test`. Windows x86_64 packaging uses Swift 6.2.3, PowerShell and the pinned SQLite build described in [Installation](Documentation/Reference/Installation.md). The Windows adapter -serves standard MCP over stdio; it does not provide a Windows host GUI. Launch +serves standard MCP over stdio and requires the user-installed official +Microsoft Visual C++ v14 x64 runtime. It does not provide a Windows host GUI. Launch `.build/debug/codex-mcp-adapter` through an MCP stdio client, never as an unbounded unattended shell command. `--help` prints usage without serving. diff --git a/Scripts/check-package.py b/Scripts/check-package.py index ab373bc..1828d9d 100644 --- a/Scripts/check-package.py +++ b/Scripts/check-package.py @@ -43,6 +43,10 @@ def verify(archive, receipt_path, manifest, destination=None): raise ValueError("Archive receipt must declare its file inventory") for name, value in inventory.items(): safe_path(name) + if receipt.get("platform") == "windows": + from windows_runtime import is_msvc_runtime + if is_msvc_runtime(PurePosixPath(name).name): + raise ValueError("Microsoft runtime DLLs must be installed separately, not bundled") if not isinstance(value, str) or not re.fullmatch(r"[0-9a-f]{64}", value): raise ValueError("Archive inventory requires exact SHA-256 digests") expected_name = validate_architectures(manifest, receipt["architectures"], receipt["platform"]) diff --git a/Scripts/check-windows-package.ps1 b/Scripts/check-windows-package.ps1 index 41c62eb..24a71d4 100644 --- a/Scripts/check-windows-package.ps1 +++ b/Scripts/check-windows-package.ps1 @@ -25,6 +25,7 @@ if ($LASTEXITCODE -ne 0) { throw 'Cannot identify acceptance source' } runnerArchitecture = $env:RUNNER_ARCH existingSwiftCommands = @((Get-Command swift -All -ErrorAction SilentlyContinue).Source) swiftToolchainInstalledByThisJob = $false + microsoftRuntime = 'system-installed prerequisite; native versions and module paths verified against the package receipt' pristineWindowsImage = $false sourceRebuilt = $false authenticatedModel = $false @@ -52,7 +53,7 @@ if ((Get-FileHash $codexBinary -Algorithm SHA256).Hash.ToLowerInvariant() -ne $m throw 'Native Codex executable checksum mismatch' } python (Join-Path $fixture 'ProtocolCheck.py') --adapter (Join-Path $relocated 'bin/codex-mcp-adapter.exe') ` - --codex $codexBinary --evidence-directory (Join-Path $evidence 'protocol') --app-local-runtime ` + --codex $codexBinary --evidence-directory (Join-Path $evidence 'protocol') --app-local-runtime --package-receipt $receiptPath ` *> (Join-Path $evidence 'protocol.log') $code = $LASTEXITCODE Get-Content (Join-Path $evidence 'protocol.log') -Tail 80 diff --git a/Scripts/windows_package.py b/Scripts/windows_package.py index e59fbd6..2379521 100644 --- a/Scripts/windows_package.py +++ b/Scripts/windows_package.py @@ -60,17 +60,23 @@ def stage_runtime(repo, binary_directory, notices, sqlite, swift, command, copy_ raise ValueError("Selected Swift runtime directories and llvm-readobj are required") report = windows_runtime.audit(binary_directory / "codex-mcp-adapter.exe", runtime_directories, Path(os.environ["SystemRoot"]) / "System32", Path(inspector)) - if any(row["role"] == "external-msvc-runtime" for row in report["libraries"]): - raise ValueError("Runtime inputs must come from the selected toolchain, not System32 redistributables") coverage = metadata["runtime_libraries"] copied = [] + prerequisites = [] for row in report["libraries"]: + if row["role"] == "external-msvc-runtime": + windows_runtime.version_tuple(row["version"]) + prerequisites.append({"name": row["name"], "minimum_version": row["version"], + "build_input_sha256": row["sha256"], "architecture": row["architecture"]}) + continue if row["role"] != "runtime": continue name = row["name"].casefold() declaration = coverage.get(name) if declaration is None: raise ValueError(f"Runtime library lacks reviewed notice coverage: {row['name']}") + if declaration["license_scope"] != "open-source" or windows_runtime.is_msvc_runtime(name): + raise ValueError(f"Runtime library is an external prerequisite: {row['name']}") source = Path(row["path"]) target = binary_directory / source.name if os.path.lexists(target): @@ -82,6 +88,10 @@ def stage_runtime(repo, binary_directory, notices, sqlite, swift, command, copy_ "notice_coverage": declaration}) if not any(row["file"].casefold() == "swiftcore.dll" for row in copied): raise ValueError("The native adapter must include its Swift runtime") + if any(windows_runtime.is_msvc_runtime(name) for name in windows_runtime.directory_files(binary_directory)): + raise ValueError("Microsoft runtime DLLs must be installed separately, not bundled") + if not prerequisites: + raise ValueError("Native runtime closure must declare its Microsoft prerequisites") copy_tree(notice_root, notices / "windows-runtime") # The original SQLite header retains its public-domain statement verbatim. sqlite_notices = notices / "sqlite" @@ -93,7 +103,9 @@ def stage_runtime(repo, binary_directory, notices, sqlite, swift, command, copy_ copy_file(repo / "Scripts/windows-sqlite.json", sqlite_notices / "source.json") return {"architecture": {"aarch64": "arm64", "x86_64": "x86_64"}[report["architecture"]], "swift_version": version[1], "libraries": copied, + "external_prerequisites": prerequisites, "sqlite_source": sqlite["receipt"]["source"], "sqlite_library_sha256": sqlite["receipt"]["librarySHA256"], "notice_metadata_sha256": windows_runtime.digest(notice_root / "sources.json"), - "system_imports": [row["name"] for row in report["libraries"] if row["role"] != "runtime"]} + "system_imports": [row["name"] for row in report["libraries"] + if row["role"] in {"windows-system", "windows-api-set"}]} diff --git a/Scripts/windows_runtime.py b/Scripts/windows_runtime.py index a38ccbb..90ac88e 100644 --- a/Scripts/windows_runtime.py +++ b/Scripts/windows_runtime.py @@ -10,6 +10,7 @@ import re import shutil import stat +import struct import subprocess @@ -25,6 +26,63 @@ def regular_file(path): raise ValueError(f"Runtime input is a link or special file: {path}") +def is_msvc_runtime(name): + return re.fullmatch(r"(?:vcruntime|msvcp|concrt|vccorlib)\d[a-z0-9_]*\.dll", name.casefold()) is not None + + +def pe_architecture(path): + regular_file(path) + with path.open("rb") as source: + header = source.read(64) + if len(header) != 64 or header[:2] != b"MZ": + raise ValueError(f"Missing PE header: {path}") + source.seek(struct.unpack_from(" 1024 * 1024: + raise ValueError(f"Missing or oversized file version resource: {path}") + buffer = ctypes.create_string_buffer(size) + if not version.GetFileVersionInfoW(str(path), 0, size, buffer): + raise ctypes.WinError(ctypes.get_last_error()) + pointer, length = wintypes.LPVOID(), wintypes.UINT() + if not version.VerQueryValueW(buffer, "\\", ctypes.byref(pointer), ctypes.byref(length)) or length.value < 52: + raise ValueError(f"Missing fixed file version: {path}") + fields = ctypes.cast(pointer, ctypes.POINTER(wintypes.DWORD)) + if fields[0] != 0xFEEF04BD: + raise ValueError(f"Invalid fixed file version signature: {path}") + return ".".join(map(str, (fields[2] >> 16, fields[2] & 0xFFFF, fields[3] >> 16, fields[3] & 0xFFFF))) + + +def version_tuple(value): + if not isinstance(value, str) or not re.fullmatch(r"\d{1,5}(?:\.\d{1,5}){3}", value): + raise ValueError("Runtime version must have four numeric components") + result = tuple(map(int, value.split("."))) + if max(result) > 65535: + raise ValueError("Runtime version component exceeds its native bound") + return result + + def imports(path, inspector): regular_file(path) result = subprocess.run([str(inspector), "--file-headers", "--coff-imports", str(path)], @@ -65,10 +123,10 @@ def resolve(name, runtime_files, system_files): regular_file(path) if len({digest(path) for path in candidates}) != 1: raise ValueError(f"Conflicting runtime DLL sources for {name}: {candidates}") - return "runtime", candidates[0] + return ("external-msvc-runtime" if is_msvc_runtime(name) else "runtime"), candidates[0] if key in system_files: # Visual C++ redistributables are not Windows OS components, even in System32. - role = ("external-msvc-runtime" if re.match(r"(?:vcruntime|msvcp|concrt)\d", key) + role = ("external-msvc-runtime" if is_msvc_runtime(name) else "windows-system") regular_file(system_files[key]) return role, system_files[key] @@ -98,6 +156,8 @@ def audit(executable, runtime_directories, system_directory, inspector): if native_arch != architecture: raise ValueError(f"Runtime architecture mismatch: {path}: {native_arch} != {architecture}") entry.update(architecture=native_arch, imports=dependencies) + if role == "external-msvc-runtime": + entry["version"] = file_version(path) queue.extend((dependency, name) for dependency in dependencies) return {"executable": str(executable), "sha256": digest(executable), "architecture": architecture, "runtime_directories": list(map(str, runtime_directories)), @@ -155,14 +215,51 @@ def loaded_modules(pid): kernel.CloseHandle(handle) -def verify_app_local_modules(modules, executable, system_directory): +def verify_prerequisites(executable, system_directory, requirements): + local = directory_files(executable.parent) + if any(is_msvc_runtime(name) for name in local): + raise ValueError("Microsoft runtime DLLs must be installed separately, not bundled") + architecture = pe_architecture(executable) + system = directory_files(system_directory) + result, seen = [], set() + if not requirements: + raise ValueError("Missing Microsoft runtime prerequisite declarations") + for requirement in requirements: + name = requirement["name"].casefold() + if not is_msvc_runtime(name) or name in seen: + raise ValueError("Invalid or duplicate Microsoft runtime prerequisite") + seen.add(name) + minimum = version_tuple(requirement["minimum_version"]) + path = system.get(name) + if path is None: + raise ValueError(f"Install the official Microsoft Visual C++ runtime: missing {name}") + if pe_architecture(path) != architecture: + raise ValueError(f"Microsoft runtime architecture mismatch: {path}") + current = file_version(path) + if version_tuple(current) < minimum: + raise ValueError(f"Update the official Microsoft Visual C++ runtime: {name} {current} < {requirement['minimum_version']}") + result.append({"name": name, "path": str(path), "version": current, + "minimum_version": requirement["minimum_version"], "architecture": architecture, + "sha256": digest(path), "role": "external-msvc-runtime"}) + return result + + +def verify_app_local_modules(modules, executable, system_directory, prerequisites=()): local = directory_files(executable.parent) + if any(is_msvc_runtime(name) for name in local): + raise ValueError("Microsoft runtime DLLs must be installed separately, not bundled") + external = {row["name"].casefold(): row for row in prerequisites} observed = set() result = [] for path in modules: name = path.name.casefold() regular_file(path) - if name in local: + if is_msvc_runtime(name): + expected = external.get(name) + if expected is None or not path.samefile(Path(expected["path"])) or digest(path) != expected["sha256"]: + raise ValueError(f"Process loaded an unverified Microsoft runtime: {path}") + role = "external-msvc-runtime" + elif name in local: if not path.samefile(local[name]): raise ValueError(f"Packaged runtime was loaded from outside the package: {path}") role = "app-local-runtime" diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index e1009c0..d005add 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -24,10 +24,12 @@ binds those texts to upstream commits and maps runtime libraries to notices. public-domain statement and the pinned source identity. The package receipt separately binds the selected runtime DLLs and compiled SQLite library. -Microsoft Visual C++ runtime files retain their separate Microsoft redistribution -terms. The applicable publisher license must permit redistribution; their -presence in the Swift toolchain does not establish that permission. See the -[Microsoft redistribution guidance](https://learn.microsoft.com/en-us/cpp/windows/redistributing-visual-cpp-files?view=msvc-170). +The Microsoft Visual C++ runtime is a user-installed prerequisite, supplied by +Microsoft's official installer. The plugin archive carries the Swift/open-source +runtime closure and records external Microsoft runtime requirements in its receipt. +The upstream provenance mapping includes Microsoft entries for identification; +those entries are not a bundled-file inventory. See the +[official runtime downloads](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170). An ad-hoc signature and checksum support local integrity checks. They do not establish official publisher provenance, Developer ID signing, notarization, or diff --git a/Tests/WindowsAdapter/ProtocolCheck.py b/Tests/WindowsAdapter/ProtocolCheck.py index 73532b8..28d04c5 100644 --- a/Tests/WindowsAdapter/ProtocolCheck.py +++ b/Tests/WindowsAdapter/ProtocolCheck.py @@ -78,7 +78,7 @@ def remove_read_only(function, path, failure): return repaired -def run(adapter, codex, evidence_directory=None, app_local_runtime=False): +def run(adapter, codex, evidence_directory=None, app_local_runtime=False, package_receipt=None): if app_local_runtime and os.name != "nt": raise ValueError("App-local runtime acceptance requires native Windows") if evidence_directory is not None: @@ -124,6 +124,18 @@ def run(adapter, codex, evidence_directory=None, app_local_runtime=False): confirmed = False phase = "executable versions" try: + prerequisites = [] + if app_local_runtime: + phase = "Microsoft runtime prerequisites" + if package_receipt is None: + raise ValueError("App-local acceptance requires the verified package receipt") + package = json.loads(package_receipt.read_text(encoding="utf-8")) + if package["files"]["bin/codex-mcp-adapter.exe"] != receipt["adapter_sha256"]: + raise ValueError("Runtime prerequisite receipt does not identify this adapter") + prerequisites = runtime.verify_prerequisites( + adapter, system_directory, package["windows_runtime"]["external_prerequisites"]) + receipt["external_prerequisites"] = prerequisites + phase = "executable versions" for executable, key in [(adapter, "adapter_version"), (codex, "codex_version")]: receipt[key] = subprocess.run([str(executable), "--version"], env=environment, capture_output=True, text=True, check=True, timeout=10).stdout.strip() @@ -154,7 +166,7 @@ def run(adapter, codex, evidence_directory=None, app_local_runtime=False): phase = f"connection {attempt + 1} native lifecycle" if app_local_runtime: receipt["module_observations"].append(runtime.verify_app_local_modules( - runtime.loaded_modules(process.pid), adapter, system_directory)) + runtime.loaded_modules(process.pid), adapter, system_directory, prerequisites)) started = client.call("thread.start") thread_id = started["thread"]["id"] origin = client.last_invocation @@ -242,9 +254,10 @@ def run(adapter, codex, evidence_directory=None, app_local_runtime=False): parser.add_argument("--codex", type=Path, required=True) parser.add_argument("--evidence-directory", type=Path) parser.add_argument("--app-local-runtime", action="store_true") + parser.add_argument("--package-receipt", type=Path) options = parser.parse_args() for executable in [options.adapter, options.codex]: if not executable.is_absolute() or not executable.is_file(): parser.error("Executables must be existing absolute file paths") print(json.dumps(run(options.adapter, options.codex, options.evidence_directory, - options.app_local_runtime), indent=2)) + options.app_local_runtime, options.package_receipt), indent=2)) diff --git a/Tests/test_windows_package.py b/Tests/test_windows_package.py index 16c9014..e09ea4c 100644 --- a/Tests/test_windows_package.py +++ b/Tests/test_windows_package.py @@ -5,12 +5,43 @@ import sys import tempfile import unittest +from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "Scripts")) import windows_package class WindowsPackageNoticesTests(unittest.TestCase): + def test_staging_copies_open_source_closure_and_records_external_runtime_floor(self): + repo = Path(__file__).resolve().parents[1] + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + source, binary, notices, include = [root / name for name in ("runtime", "bin", "notices", "include")] + for path in (source, binary, notices, include): + path.mkdir() + (source / "swiftCore.dll").write_bytes(b"swift") + (source / "VCRUNTIME140.dll").write_bytes(b"microsoft") + (binary / "codex-mcp-adapter.exe").write_bytes(b"adapter") + header = include / "sqlite3.h" + header.write_bytes(b"sqlite header") + rows = [] + for path in source.iterdir(): + rows.append({"name": path.name, "role": "external-msvc-runtime" if path.name.startswith("VC") else "runtime", + "path": str(path), "sha256": windows_package.windows_runtime.digest(path), + "bytes": path.stat().st_size, "architecture": "x86_64", "version": "14.44.35211.0"}) + report = {"architecture": "x86_64", "libraries": rows} + sqlite = {"includeDirectory": str(include), "receipt": {"source": "fixture", + "librarySHA256": "0" * 64, "headerSHA256": windows_package.windows_runtime.digest(header)}} + with patch.dict("os.environ", {"PATH": str(source), "SystemRoot": str(root)}), \ + patch.object(windows_package.shutil, "which", return_value="inspector"), \ + patch.object(windows_package.windows_runtime, "audit", return_value=report): + result = windows_package.stage_runtime(repo, binary, notices, sqlite, "swift", + lambda *_: "Swift version 6.2.3", shutil.copyfile, shutil.copytree) + self.assertEqual([row["file"] for row in result["libraries"]], ["swiftCore.dll"]) + self.assertFalse((binary / "VCRUNTIME140.dll").exists()) + self.assertEqual(result["external_prerequisites"][0]["minimum_version"], "14.44.35211.0") + self.assertEqual(result["system_imports"], []) + def test_notice_bytes_and_component_coverage_bind_to_the_owned_sources(self): source = Path(__file__).resolve().parents[1] / "Vendor/SwiftWindowsRuntime" original = windows_package.notice_sources(source) diff --git a/Tests/test_windows_runtime.py b/Tests/test_windows_runtime.py index dccc2b3..24032b6 100644 --- a/Tests/test_windows_runtime.py +++ b/Tests/test_windows_runtime.py @@ -1,6 +1,8 @@ import importlib.util +import os from pathlib import Path import tempfile +import struct import unittest from unittest.mock import patch @@ -30,7 +32,8 @@ def test_recursive_shared_and_cyclic_imports_preserve_all_owners(self): "swiftCore.dll": ["Foundation.dll", "KERNEL32.dll"], "VCRUNTIME140.dll": ["api-ms-win-crt-runtime-l1-1-0.dll"], } - with patch.object(runtime, "imports", side_effect=lambda path, _: ("x86_64", graph[path.name])): + with patch.object(runtime, "imports", side_effect=lambda path, _: ("x86_64", graph[path.name])), \ + patch.object(runtime, "file_version", return_value="14.44.35211.0"): report = runtime.audit(executable, [libraries], system, Path("inspector")) rows = {row["name"]: row for row in report["libraries"]} self.assertEqual(len(rows), 5) @@ -39,6 +42,58 @@ def test_recursive_shared_and_cyclic_imports_preserve_all_owners(self): self.assertEqual(rows["KERNEL32.dll"]["role"], "windows-system") self.assertFalse(report["relocation_verified"]) + def test_microsoft_runtime_remains_external_when_found_in_toolchain(self): + with tempfile.TemporaryDirectory() as directory: + source = Path(directory) / "VCRUNTIME140.dll" + source.write_bytes(b"toolchain") + self.assertEqual(runtime.resolve(source.name, [{source.name.lower(): source}], {}), + ("external-msvc-runtime", source)) + + def test_prerequisites_reject_missing_old_wrong_architecture_and_bundled_runtime(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + package, system = root / "package", root / "system" + package.mkdir() + system.mkdir() + def pe(path, machine): + header = bytearray(64) + header[:2] = b"MZ" + struct.pack_into("