Align skill package checks and include notices in archives
Check instruction packages / check (push) Successful in 2s

This commit is contained in:
admin committed 2026-10-05 12:39:10 -04:00
1 parent 6e6a691b68
commit 370416b87b
3 files changed
+42 -7

No files matched your search

+1 -1
View File
@@ -21,7 +21,7 @@ python -m tools.packages check examples/starter-planner
python -m tools.packages pack examples/starter-planner dist/starter-planner.zip python -m tools.packages pack examples/starter-planner dist/starter-planner.zip
``` ```
Replace `examples/starter-planner` in the last two commands with your package directory. The `pack` command writes a reproducible ZIP and prints its SHA-256. The check returns the owner's package identity digest. Both use only Python 3's standard library and this repository's source. Include `LICENSE` and `NOTICE.md` when redistributing the package. Replace `examples/starter-planner` in the last two commands with your package directory. The `pack` command writes a reproducible ZIP containing the package, `LICENSE`, and `NOTICE.md`, then prints its SHA-256. The check returns the owner's package identity digest. Both use only Python 3's standard library and this repository's source. Preserve the license and notice when redistributing the package or fork.
To import a user fork, place its closed package directory under the owner's configured trusted root, use Settings → Skills to import the relative `SKILL.md` path under a new unique ID, and assign it to the matching future role step. Saving a new assignment takes effect after owner restart; existing sessions retain their captured bytes. The Settings catalog displays source, version, digest, role, step, and pending versus effective assignment. The owner's admission remains authoritative even when these local checks pass. To import a user fork, place its closed package directory under the owner's configured trusted root, use Settings → Skills to import the relative `SKILL.md` path under a new unique ID, and assign it to the matching future role step. Saving a new assignment takes effect after owner restart; existing sessions retain their captured bytes. The Settings catalog displays source, version, digest, role, step, and pending versus effective assignment. The owner's admission remains authoritative even when these local checks pass.
+26
View File
@@ -3,6 +3,7 @@ import hashlib
import shutil import shutil
import tempfile import tempfile
import unittest import unittest
import zipfile
from pathlib import Path from pathlib import Path
from tools.packages import PackageError, check_package, package_archive from tools.packages import PackageError, check_package, package_archive
@@ -70,6 +71,31 @@ class PackageTests(unittest.TestCase):
with self.assertRaises(PackageError): with self.assertRaises(PackageError):
check_package(package) check_package(package)
def test_rejects_owner_incompatible_frontmatter_values(self):
for old, new in (
("metadata:\n", "disable-model-invocation: yes\nmetadata:\n"),
("description: Check a Planner's issue scope and evidence before drafting a Plan.", "description: "),
):
with self.subTest(new=new), tempfile.TemporaryDirectory() as directory:
package = Path(directory) / "planner-scope"
shutil.copytree(ROOT / "packages/planner-scope", package)
skill = package / "SKILL.md"
skill.write_text(skill.read_text().replace(old, new))
manifest = package / "relayview-skill.json"
data = json.loads(manifest.read_text())
next(item for item in data["files"] if item["path"] == "SKILL.md")["sha256"] = hashlib.sha256(skill.read_bytes()).hexdigest()
manifest.write_text(json.dumps(data))
with self.assertRaises(PackageError):
check_package(package)
def test_archive_carries_license_and_notice(self):
with tempfile.TemporaryDirectory() as directory:
archive_path = Path(directory) / "planner-scope.zip"
package_archive(ROOT / "packages/planner-scope", archive_path)
with zipfile.ZipFile(archive_path) as archive:
self.assertEqual(archive.read("LICENSE"), (ROOT / "LICENSE").read_bytes())
self.assertEqual(archive.read("NOTICE.md"), (ROOT / "NOTICE.md").read_bytes())
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+15 -6
View File
@@ -117,9 +117,17 @@ def check_package(path):
_fail(f"{path}: unsupported skill metadata") _fail(f"{path}: unsupported skill metadata")
if len(top_keys) != len(set(top_keys)) or set(top_keys) - {"name", "description", "metadata", "disable-model-invocation"} or len(nested_keys) != len(set(nested_keys)) or set(nested_keys) != {"relayview-display-name", "relayview-version"}: if len(top_keys) != len(set(top_keys)) or set(top_keys) - {"name", "description", "metadata", "disable-model-invocation"} or len(nested_keys) != len(set(nested_keys)) or set(nested_keys) != {"relayview-display-name", "relayview-version"}:
_fail(f"{path}: unsupported skill metadata") _fail(f"{path}: unsupported skill metadata")
values = dict(line.split(": ", 1) for line in frontmatter.splitlines() if not line.startswith(" ") and ": " in line)
if "disable-model-invocation" in values and values["disable-model-invocation"] not in ("true", "false"):
_fail(f"{path}: disable-model-invocation must be a boolean")
expected_name = path.name expected_name = path.name
if not re.search(rf"(?m)^name: {re.escape(expected_name)}$", frontmatter) or not re.search(r"(?m)^description: .+$", frontmatter) or not re.search(r"(?m)^ relayview-display-name: .+$", frontmatter) or not re.search(r"(?m)^ relayview-version: [0-9]+\.[0-9]+\.[0-9]+$", frontmatter): if not re.search(rf"(?m)^name: {re.escape(expected_name)}$", frontmatter) or not re.search(r"(?m)^description: .+$", frontmatter) or not re.search(r"(?m)^ relayview-display-name: .+$", frontmatter) or not re.search(r"(?m)^ relayview-version: [0-9]+\.[0-9]+\.[0-9]+$", frontmatter):
_fail(f"{path}: missing or incompatible skill metadata") _fail(f"{path}: missing or incompatible skill metadata")
if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", expected_name) or len(expected_name) > 64 or len(values["description"]) > 1024:
_fail(f"{path}: skill name or description exceeds owner bounds")
display_name = next(line.split(": ", 1)[1] for line in frontmatter.splitlines() if line.startswith(" relayview-display-name: "))
if len(display_name) > 128:
_fail(f"{path}: display name exceeds owner bounds")
digest = hashlib.sha256(b"relayview-instruction-package-v1\0" + _frame(raw_manifest)) digest = hashlib.sha256(b"relayview-instruction-package-v1\0" + _frame(raw_manifest))
for name in sorted(declared): for name in sorted(declared):
digest.update(_frame(name.encode("utf-8"))) digest.update(_frame(name.encode("utf-8")))
@@ -132,12 +140,13 @@ def package_archive(path, destination):
check_package(path) check_package(path)
destination.parent.mkdir(parents=True, exist_ok=True) destination.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(destination, "w", compression=zipfile.ZIP_STORED) as archive: with zipfile.ZipFile(destination, "w", compression=zipfile.ZIP_STORED) as archive:
for file in sorted(path.rglob("*")): root = Path(__file__).resolve().parents[1]
if file.is_file(): files = [(name, root / name) for name in ("LICENSE", "NOTICE.md")]
name = f"{path.name}/{file.relative_to(path).as_posix()}" files += [(f"{path.name}/{file.relative_to(path).as_posix()}", file) for file in sorted(path.rglob("*")) if file.is_file()]
info = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0)) for name, file in files:
info.external_attr = 0o100644 << 16 info = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0))
archive.writestr(info, file.read_bytes()) info.external_attr = 0o100644 << 16
archive.writestr(info, file.read_bytes())
return hashlib.sha256(destination.read_bytes()).hexdigest() return hashlib.sha256(destination.read_bytes()).hexdigest()