From db73a4359b8cc365daa15b46731d3792f4b8bf4f Mon Sep 17 00:00:00 2001 From: Alex Ezell Date: Fri, 2 Oct 2026 15:56:06 -0500 Subject: [PATCH] Make directory release packaging reproducible --- .gitignore | 1 + README.md | 6 + plugins/sprites/.codex-plugin/plugin.json | 11 +- plugins/sprites/assets/fly-logo.png | Bin 0 -> 47948 bytes release/README.md | 50 ++++++ release/directory.json | 5 + scripts/check_repository.py | 1 + scripts/package_plugin.py | 198 ++++++++++++++++++++++ tests/test_package_plugin.py | 115 +++++++++++++ 9 files changed, 382 insertions(+), 5 deletions(-) create mode 100644 plugins/sprites/assets/fly-logo.png create mode 100644 release/README.md create mode 100644 release/directory.json create mode 100644 scripts/package_plugin.py create mode 100644 tests/test_package_plugin.py diff --git a/.gitignore b/.gitignore index c9c91c4..6973fec 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules/ dist/ tmp/ *.log +__pycache__/ diff --git a/README.md b/README.md index ff39812..03ca2f0 100644 --- a/README.md +++ b/README.md @@ -127,3 +127,9 @@ If authentication fails during plugin install or first use, retry the plugin aut If a command inside a sprite cannot reach the network, ask Codex to inspect the sprite network policy before changing it. If a web service is not reachable, ask Codex to inspect the sprite's services, logs, and configured HTTP port. + +## Packaging a Directory Update + +Run `python3 scripts/package_plugin.py` to build and validate the upload ZIP. +See [directory release packaging](release/README.md) for the published-version +baseline, upload-specific settings, and remaining portal checks. diff --git a/plugins/sprites/.codex-plugin/plugin.json b/plugins/sprites/.codex-plugin/plugin.json index 1021ac3..58baf14 100644 --- a/plugins/sprites/.codex-plugin/plugin.json +++ b/plugins/sprites/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "sprites", - "version": "0.2.0", + "version": "1.0.1", "description": "Use Sprites from Codex to create, inspect, and operate remote isolated development environments.", "author": { "name": "Fly.io", @@ -18,7 +18,7 @@ "mcpServers": "./.mcp.json", "interface": { "displayName": "Sprites", - "shortDescription": "Manage remote development sprites from Codex.", + "shortDescription": "Give Codex its own computer", "longDescription": "Create, list, inspect, and operate remote Sprites environments from Codex. Select individual Sprites in supported desktop composers and open a read-only Sprite Inspector for services, checkpoints, and recent logs.", "developerName": "Fly.io", "category": "Developer Tools", @@ -34,8 +34,9 @@ "Use Sprites to create a new remote environment for this repo and run the tests.", "Use Sprites to inspect services and logs for a selected sprite." ], - "brandColor": "#8AE234", - "composerIcon": "./assets/sprites-logo.svg", - "logo": "./assets/sprites-logo.svg" + "brandColor": "#16A34A", + "composerIcon": "./assets/fly-logo.png", + "logo": "./assets/fly-logo.png", + "supportURL": "https://docs.fly.io/about/support" } } diff --git a/plugins/sprites/assets/fly-logo.png b/plugins/sprites/assets/fly-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..69729ffeb561240daa615568830f2ad05b77ab50 GIT binary patch literal 47948 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

uW?2Lg5^_r?mkih5*OFL!T|{6IZBL* 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 literal 0 HcmV?d00001 diff --git a/release/README.md b/release/README.md new file mode 100644 index 0000000..e68dcda --- /dev/null +++ b/release/README.md @@ -0,0 +1,50 @@ +# Directory release packaging + +The last confirmed published directory version is **1.0.0**. The **1.0.1** +package is prepared and pending publication; preparing or uploading it does not +mean it has been approved or published. A previous upload labeled 0.2.0 entered +review before the published version was checked. + +Build the directory ZIP from the repository root with Python 3.12 or newer: + +```sh +python3 scripts/package_plugin.py +``` + +The output is `dist/sprites-1.0.1.zip` for the current version. ZIP files and +staging copies stay in ignored `dist/`; commit the source and release settings. +No additional Python packages or network access are needed to build the ZIP. + +The local manifest is the source for the version and listing. Before each +release, confirm the latest published version in the directory and update +`published_version` in [directory.json](directory.json). Increase the source +manifest version above it, and update `release_notes` for the intended release. +An explicit `--published-version 1.0.1` can override the recorded baseline for a +single build. This check cannot discover remote publication changes itself. + +The packager preserves the local plugin name `sprites` and local attribution +headers. In the upload copy only, it uses the verified directory identity +`app-6a8485b5beac8191954e37241acffe6d` in both manifests and the archive folder, +generates portable `plugin.json` and `mcp.json`, and removes `headers` and +`http_headers` from both MCP configurations. The directory rejected those +headers during existing-connection setup. Authentication belongs in the portal's +connection settings; no credentials belong in the package. + +Local checks cover version progression, matching manifests, the directory ID, +MCP endpoint and headers, subtitle length, default prompts, required HTTPS links, +brand color contrast against white, icon paths and PNG dimensions, skill presence, +and the final ZIP contents. The build uses fixed ZIP metadata so identical source +produces identical bytes. These checks cover known upload constraints, not every +portal rule. They do not verify live URLs, remote tool behavior, connection state, +review materials, scans, attestations, or publication status. + +After uploading to the existing entry, inspect its saved listing, connection, +review materials, and publication settings. Review and publication are separate +steps. Replacing an in-review submission requires cancelling review first; do +not infer approval to cancel from a request to prepare a ZIP. + +The logo is Fly.io's official color brandmark from +[Fly.io's brand assets](https://docs.fly.io/about/brand). The committed 512 x 512 +PNG was rendered from the official SVG with transparent square padding. It is +used for both listing and composer icons. The green brand color `#16A34A` has +approximately 3.30:1 contrast against white, exceeding the directory's 2:1 minimum. diff --git a/release/directory.json b/release/directory.json new file mode 100644 index 0000000..41a0ad2 --- /dev/null +++ b/release/directory.json @@ -0,0 +1,5 @@ +{ + "plugin_id": "app-6a8485b5beac8191954e37241acffe6d", + "published_version": "1.0.0", + "release_notes": "Adds guidance for selecting individual Sprites in supported desktop composers and opening the Sprite Inspector to inspect services, checkpoints, and recent logs. Verifies selected Sprite identities before remote operations." +} diff --git a/scripts/check_repository.py b/scripts/check_repository.py index acf9697..92915b6 100644 --- a/scripts/check_repository.py +++ b/scripts/check_repository.py @@ -22,6 +22,7 @@ ".ruff_cache", ".venv", "__pycache__", + "dist", } TEXT_SUFFIXES = { ".json", diff --git a/scripts/package_plugin.py b/scripts/package_plugin.py new file mode 100644 index 0000000..996c8b0 --- /dev/null +++ b/scripts/package_plugin.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""Build and preflight the directory upload without changing local plugin configuration.""" + +from __future__ import annotations + +import argparse +import copy +import json +import re +import struct +import zipfile +from pathlib import Path, PurePosixPath +from urllib.parse import urlparse + +ROOT = Path(__file__).resolve().parents[1] + + +def require(condition: bool, message: str) -> None: + if not condition: + raise ValueError(message) + + +def stable_version(value: str) -> tuple[int, ...]: + require( + bool(re.fullmatch(r"(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)", value)), + f"Expected a stable major.minor.patch version, got {value!r}", + ) + return tuple(map(int, value.split("."))) + + +def contrast_against_white(color: str) -> float: + require(bool(re.fullmatch(r"#[0-9a-fA-F]{6}", color)), "brandColor must be #RRGGBB") + channels = [int(color[i : i + 2], 16) / 255 for i in (1, 3, 5)] + linear = [v / 12.92 if v <= 0.04045 else ((v + 0.055) / 1.055) ** 2.4 for v in channels] + luminance = sum(v * weight for v, weight in zip(linear, (0.2126, 0.7152, 0.0722))) + return 1.05 / (luminance + 0.05) + + +def json_bytes(data: dict) -> bytes: + return (json.dumps(data, indent=2) + "\n").encode() + + +def asset_path(value: str) -> str: + path = PurePosixPath(value) + require(not path.is_absolute() and ".." not in path.parts, "Asset must stay inside package") + require(path.parts[0] == "assets", "Icons must be in assets/") + return path.as_posix() + + +def validate_package(files: dict[str, bytes], release: dict) -> None: + """Check the effective manifests and known portal rules in the actual archive content.""" + manifest = json.loads(files["plugin.json"]) + legacy = json.loads(files[".codex-plugin/plugin.json"]) + for item in (manifest, legacy): + require(item["name"] == release["plugin_id"], "Plugin name must match directory ID") + require( + stable_version(item["version"]) > stable_version(release["published_version"]), + "Package version must exceed published_version", + ) + require(not item.get("apps"), "Upload cannot declare app bindings") + require( + not item.get("extensions", {}).get("com.openai", {}).get("apps"), + "Upload cannot declare app bindings", + ) + require(manifest["version"] == legacy["version"], "Manifest versions must match") + interface = manifest["extensions"]["com.openai"]["interface"] + require(interface == legacy["interface"], "Manifest interfaces must match") + for field, limit in (("displayName", 30), ("shortDescription", 30), ("longDescription", 4000)): + require(0 < len(interface[field]) <= limit, f"{field} must be 1-{limit} characters") + require(contrast_against_white(interface["brandColor"]) >= 2, "brandColor needs 2:1 contrast") + prompts = interface["defaultPrompt"] + prompts = [prompts] if isinstance(prompts, str) else prompts + require(1 <= len(prompts) <= 3, "Expected 1-3 default prompts") + require( + all(p.strip() and len(p) <= 128 and "\n" not in p for p in prompts), + "Prompts must be nonblank single lines of at most 128 characters", + ) + require(len({" ".join(p.split()) for p in prompts}) == len(prompts), "Duplicate prompts") + for field in ("websiteURL", "supportURL", "privacyPolicyURL", "termsOfServiceURL"): + url = urlparse(interface[field]) + require( + url.scheme == "https" and bool(url.hostname) and not url.username and not url.password, + f"{field} must be an HTTPS URL without credentials", + ) + for field in ("logo", "composerIcon"): + path = asset_path(interface[field]) + require(path in files, f"Missing icon: {path}") + data = files[path] + require( + len(data) >= 24 and data[:8] == b"\x89PNG\r\n\x1a\n" and data[12:16] == b"IHDR", + "Icons must be PNG files", + ) + width, height = struct.unpack(">II", data[16:24]) + minimum = 256 if field == "logo" else 48 + require(minimum <= width == height <= 4096, "Icons must be square and correctly sized") + require(len(data) <= 5 * 1024 * 1024, "Icon exceeds 5 MiB") + portable = json.loads(files["mcp.json"])["mcpServers"] + compatibility = json.loads(files[".mcp.json"])["mcpServers"] + require(set(portable) == set(compatibility) == {"sprites"}, "Unexpected MCP server inventory") + for servers in (portable, compatibility): + server = servers["sprites"] + require("headers" not in server and "http_headers" not in server, "MCP headers unsupported") + require(server["url"] == "https://sprites.dev/mcp", "Keep the existing MCP endpoint") + require( + set(portable["sprites"]) == {"type", "url"} + and portable["sprites"]["type"] == "streamable-http", + "Unsupported portable MCP settings", + ) + require(".app.json" not in files, "Upload cannot include .app.json") + require("skills/sprites/SKILL.md" in files, "Missing Sprites skill") + require(legacy["skills"] == "./skills/", "Unexpected legacy skill path") + require(legacy["mcpServers"] == "./.mcp.json", "Unexpected legacy MCP path") + + +def build_package(root: Path, output: Path, published_version: str | None = None) -> Path: + source = root / "plugins/sprites" + release = json.loads((root / "release/directory.json").read_text()) + if published_version: + release["published_version"] = published_version + require( + bool(re.fullmatch(r"app-[0-9a-f]{32}", release["plugin_id"])), + "Expected the verified directory plugin ID", + ) + legacy = json.loads((source / ".codex-plugin/plugin.json").read_text()) + legacy["name"] = release["plugin_id"] + manifest = { + k: copy.deepcopy(v) + for k, v in legacy.items() + if k not in ("skills", "mcpServers", "interface") + } + manifest["$schema"] = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json" + manifest["extensions"] = { + "com.openai": { + "interface": copy.deepcopy(legacy["interface"]), + "publication": {"release_notes": release["release_notes"]}, + } + } + config = json.loads((source / ".mcp.json").read_text()) + require(set(config["mcpServers"]) == {"sprites"}, "Unexpected source MCP server inventory") + server = config["mcpServers"]["sprites"] + require( + set(server) <= {"url", "http_headers", "headers", "tool_timeout_sec"}, + "Unsupported source MCP setting; review before packaging", + ) + server.pop("http_headers", None) + server.pop("headers", None) + portable = { + "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + "mcpServers": {"sprites": {"type": "streamable-http", "url": server["url"]}}, + } + files = { + "plugin.json": json_bytes(manifest), + ".codex-plugin/plugin.json": json_bytes(legacy), + "mcp.json": json_bytes(portable), + ".mcp.json": json_bytes(config), + "LICENSE": (root / "LICENSE").read_bytes(), + } + paths = {asset_path(legacy["interface"][field]) for field in ("logo", "composerIcon")} + paths.update(p.relative_to(source).as_posix() for p in (source / "skills").rglob("*.md")) + for relative in sorted(paths): + path = source / relative + require(path.resolve().is_relative_to(source.resolve()), "File escapes plugin directory") + require(not path.is_symlink(), "Package cannot contain symlinks") + files[relative] = path.read_bytes() + validate_package(files, release) + output.mkdir(parents=True, exist_ok=True) + archive = output / f"sprites-{manifest['version']}.zip" + # Fixed metadata and sorted paths make repeated builds byte-for-byte reproducible. + with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as bundle: + for name, data in sorted(files.items()): + info = zipfile.ZipInfo(f"{release['plugin_id']}/{name}", (2020, 1, 1, 0, 0, 0)) + info.compress_type = zipfile.ZIP_DEFLATED + info.external_attr = 0o100644 << 16 + bundle.writestr(info, data) + with zipfile.ZipFile(archive) as bundle: + require(bundle.testzip() is None, "ZIP integrity check failed") + prefix = release["plugin_id"] + "/" + actual = {name.removeprefix(prefix): bundle.read(name) for name in bundle.namelist()} + require(actual == files, "ZIP content differs from prepared package") + validate_package(actual, release) + return archive + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output-dir", type=Path, default=ROOT / "dist") + parser.add_argument("--published-version", help="Override the last confirmed published version") + args = parser.parse_args() + try: + archive = build_package(ROOT, args.output_dir, args.published_version) + except (ValueError, KeyError, OSError) as exc: + parser.exit(1, f"Packaging failed: {exc}\n") + print(f"Built and locally validated {archive}") + print("Portal acceptance, review, and publication are not verified by these checks.") + + +if __name__ == "__main__": + main() diff --git a/tests/test_package_plugin.py b/tests/test_package_plugin.py new file mode 100644 index 0000000..494bdee --- /dev/null +++ b/tests/test_package_plugin.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +import json +import tempfile +import unittest +import zipfile +from pathlib import Path + +from scripts.package_plugin import ROOT, build_package, json_bytes, validate_package + + +class DirectoryPackageTests(unittest.TestCase): + def setUp(self) -> None: + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.output = Path(self.temp.name) + self.release = json.loads((ROOT / "release/directory.json").read_text()) + self.archive = build_package(ROOT, self.output) + with zipfile.ZipFile(self.archive) as bundle: + prefix = self.release["plugin_id"] + "/" + self.files = { + name.removeprefix(prefix): bundle.read(name) for name in bundle.namelist() + } + + def edit_json(self, path: str, edit) -> None: + data = json.loads(self.files[path]) + edit(data) + self.files[path] = json_bytes(data) + + def edit_interfaces(self, **changes) -> None: + self.edit_json( + "plugin.json", + lambda m: m["extensions"]["com.openai"]["interface"].update(changes), + ) + self.edit_json(".codex-plugin/plugin.json", lambda m: m["interface"].update(changes)) + + def test_package_preserves_runtime_skill_and_local_configuration(self) -> None: + source = ROOT / "plugins/sprites" + self.assertEqual( + self.files["skills/sprites/SKILL.md"], (source / "skills/sprites/SKILL.md").read_bytes() + ) + local = json.loads((source / ".codex-plugin/plugin.json").read_text()) + self.assertEqual(local["name"], "sprites") + server = json.loads((source / ".mcp.json").read_text())["mcpServers"]["sprites"] + self.assertEqual(server["http_headers"]["Fly-Client-Agent"], "codex") + self.assertEqual(server["http_headers"]["Fly-Client-Interactive"], "false") + self.assertNotIn( + "http_headers", json.loads(self.files[".mcp.json"])["mcpServers"]["sprites"] + ) + self.assertNotIn("headers", json.loads(self.files["mcp.json"])["mcpServers"]["sprites"]) + + def test_reproducible_archive(self) -> None: + first = self.archive.read_bytes() + self.assertEqual(build_package(ROOT, self.output).read_bytes(), first) + + def test_rejects_wrong_directory_identity(self) -> None: + self.edit_json("plugin.json", lambda m: m.update(name="sprites")) + with self.assertRaisesRegex(ValueError, "directory ID"): + validate_package(self.files, self.release) + + def test_rejects_non_increasing_version(self) -> None: + candidate = json.loads(self.files["plugin.json"])["version"] + for published in (candidate, "99.0.0"): + with self.subTest(published=published): + with self.assertRaisesRegex(ValueError, "exceed published_version"): + build_package(ROOT, self.output, published) + + def test_rejects_mismatched_manifest_versions(self) -> None: + self.edit_json(".codex-plugin/plugin.json", lambda m: m.update(version="2.0.0")) + with self.assertRaisesRegex(ValueError, "versions must match"): + validate_package(self.files, self.release) + + def test_rejects_headers_in_either_configuration(self) -> None: + for path, key in (("mcp.json", "headers"), (".mcp.json", "http_headers")): + with self.subTest(path=path): + original = self.files[path] + self.edit_json(path, lambda m: m["mcpServers"]["sprites"].update({key: {}})) + with self.assertRaisesRegex(ValueError, "headers unsupported"): + validate_package(self.files, self.release) + self.files[path] = original + + def test_rejects_previous_low_contrast_color(self) -> None: + self.edit_interfaces(brandColor="#8AE234") + with self.assertRaisesRegex(ValueError, "2:1 contrast"): + validate_package(self.files, self.release) + + def test_rejects_previous_long_subtitle(self) -> None: + self.edit_interfaces(shortDescription="Manage remote development sprites from Codex.") + with self.assertRaisesRegex(ValueError, "shortDescription"): + validate_package(self.files, self.release) + + def test_rejects_missing_logo(self) -> None: + self.edit_interfaces(logo="./assets/missing.png") + with self.assertRaisesRegex(ValueError, "Missing icon"): + validate_package(self.files, self.release) + + def test_rejects_asset_path_escape(self) -> None: + self.edit_interfaces(logo="../outside.png") + with self.assertRaisesRegex(ValueError, "inside package"): + validate_package(self.files, self.release) + + def test_rejects_non_square_logo(self) -> None: + name = "assets/fly-logo.png" + data = bytearray(self.files[name]) + data[20:24] = (256).to_bytes(4, "big") + self.files[name] = bytes(data) + with self.assertRaisesRegex(ValueError, "square"): + validate_package(self.files, self.release) + + def test_rejects_changed_endpoint(self) -> None: + self.edit_json( + "mcp.json", lambda m: m["mcpServers"]["sprites"].update(url="https://example.com/mcp") + ) + with self.assertRaisesRegex(ValueError, "existing MCP endpoint"): + validate_package(self.files, self.release)