180 lines
8.3 KiB
Python
180 lines
8.3 KiB
Python
"""Dependency-free checks and reproducible archives for instruction packages."""
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import stat
|
|
import struct
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
|
|
class PackageError(ValueError):
|
|
pass
|
|
|
|
|
|
def _fail(message):
|
|
raise PackageError(message)
|
|
|
|
|
|
def _text(data, label):
|
|
try:
|
|
value = data.decode("utf-8")
|
|
except UnicodeDecodeError:
|
|
_fail(f"{label}: invalid UTF-8")
|
|
if not value or value.startswith("\ufeff") or "\x00" in value:
|
|
_fail(f"{label}: invalid text")
|
|
return value
|
|
|
|
|
|
def _frame(value):
|
|
return struct.pack(">I", len(value)) + value
|
|
|
|
|
|
def check_package(path):
|
|
path = Path(path)
|
|
if path.is_symlink() or not path.is_dir():
|
|
_fail(f"{path}: package must be a directory")
|
|
sidecar = path / "relayview-skill.json"
|
|
if sidecar.is_symlink() or not sidecar.is_file() or sidecar.stat().st_size > 65536:
|
|
_fail(f"{path}: missing or invalid sidecar")
|
|
raw_manifest = sidecar.read_bytes()
|
|
try:
|
|
manifest = json.loads(_text(raw_manifest, str(sidecar)))
|
|
except (ValueError, TypeError) as error:
|
|
_fail(f"{sidecar}: invalid JSON: {error}")
|
|
if not isinstance(manifest, dict) or (
|
|
(manifest.get("schemaVersion") == 1 and set(manifest) != {"schemaVersion", "source", "role", "step", "files"}) or
|
|
(manifest.get("schemaVersion") == 2 and set(manifest) != {"schemaVersion", "source", "role", "step", "files", "issueClaim"}) or
|
|
manifest.get("schemaVersion") not in (1, 2)
|
|
):
|
|
_fail(f"{path}: unsupported manifest fields")
|
|
source = manifest["source"]
|
|
if not isinstance(source, dict) or set(source) != {"kind", "reference"}:
|
|
_fail(f"{path}: invalid source")
|
|
if source["kind"] not in ("first-party", "user") or not isinstance(source["reference"], str) or not re.fullmatch(r"[A-Za-z0-9._:-]{1,128}", source["reference"]):
|
|
_fail(f"{path}: invalid source identity")
|
|
if (manifest["role"], manifest["step"]) not in (("planner", "planning"), ("implementer", "implementation"), ("reviewer", "review")):
|
|
_fail(f"{path}: incompatible role or step")
|
|
if manifest["schemaVersion"] == 2:
|
|
claim = manifest["issueClaim"]
|
|
label = claim.get("statusLabel") if isinstance(claim, dict) else None
|
|
if (manifest["role"], manifest["step"]) != ("planner", "planning") or not isinstance(claim, dict) or set(claim) != {"statusLabel"} or (
|
|
label is not None and (not isinstance(label, str) or not re.fullmatch(r"status/[a-z][a-z0-9-]{0,63}", label) or label == "status/ready-for-agent")
|
|
):
|
|
_fail(f"{path}: invalid issue claim policy")
|
|
files = manifest["files"]
|
|
if not isinstance(files, list) or not 1 <= len(files) <= 16:
|
|
_fail(f"{path}: invalid file list")
|
|
declared = {}
|
|
for item in files:
|
|
if not isinstance(item, dict) or set(item) != {"path", "sha256"}:
|
|
_fail(f"{path}: invalid file entry")
|
|
name, digest = item["path"], item["sha256"]
|
|
if not isinstance(name, str) or len(name) > 256 or "\\" in name or any(part in ("", ".", "..") for part in name.split("/")) or name.startswith("/"):
|
|
_fail(f"{path}: invalid file path")
|
|
if name != "SKILL.md" and not name.endswith((".md", ".txt")):
|
|
_fail(f"{path}: unsupported package content")
|
|
if name in declared or not isinstance(digest, str) or not re.fullmatch(r"[a-f0-9]{64}", digest):
|
|
_fail(f"{path}: duplicate file or invalid digest")
|
|
declared[name] = digest
|
|
if "SKILL.md" not in declared:
|
|
_fail(f"{path}: SKILL.md is required")
|
|
observed = {}
|
|
total = len(raw_manifest)
|
|
entry_count = 0
|
|
for file in path.rglob("*"):
|
|
entry_count += 1
|
|
relative = file.relative_to(path).as_posix()
|
|
if entry_count > 64 or len(file.relative_to(path).parts) > 8 or file.is_symlink():
|
|
_fail(f"{path}: package entry exceeds bounds or is a link")
|
|
if file.is_dir():
|
|
continue
|
|
if not file.is_file() or file.stat().st_mode & 0o111:
|
|
_fail(f"{path}: executable or unsupported file")
|
|
if relative == "relayview-skill.json":
|
|
continue
|
|
if relative not in declared:
|
|
_fail(f"{path}: undeclared file {relative}")
|
|
raw = file.read_bytes()
|
|
total += len(raw)
|
|
if not 1 <= len(raw) <= 1000000 or total > 131072:
|
|
_fail(f"{path}: package exceeds text bounds")
|
|
_text(raw, relative)
|
|
if hashlib.sha256(raw).hexdigest() != declared[relative]:
|
|
_fail(f"{path}: changed file {relative}")
|
|
observed[relative] = raw
|
|
if set(observed) != set(declared):
|
|
_fail(f"{path}: missing declared file")
|
|
skill = _text(observed["SKILL.md"], "SKILL.md")
|
|
match = re.match(r"\A---\r?\n(.*?)\r?\n---(?:\r?\n|\Z)", skill, re.S)
|
|
if not match:
|
|
_fail(f"{path}: invalid SKILL.md frontmatter")
|
|
frontmatter = match.group(1)
|
|
top_keys = []
|
|
nested_keys = []
|
|
in_metadata = False
|
|
for line in frontmatter.splitlines():
|
|
if line.startswith(" "):
|
|
if not in_metadata or not re.fullmatch(r" [a-z-]+: .+", line):
|
|
_fail(f"{path}: unsupported skill metadata")
|
|
nested_keys.append(line[2:].split(":", 1)[0])
|
|
elif re.match(r"^[a-z-]+:", line):
|
|
key = line.split(":", 1)[0]
|
|
top_keys.append(key)
|
|
in_metadata = key == "metadata"
|
|
else:
|
|
_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"}:
|
|
_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
|
|
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")
|
|
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))
|
|
for name in sorted(declared):
|
|
digest.update(_frame(name.encode("utf-8")))
|
|
digest.update(_frame(observed[name]))
|
|
return digest.hexdigest()
|
|
|
|
|
|
def package_archive(path, destination):
|
|
path, destination = Path(path), Path(destination)
|
|
check_package(path)
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
with zipfile.ZipFile(destination, "w", compression=zipfile.ZIP_STORED) as archive:
|
|
root = Path(__file__).resolve().parents[1]
|
|
files = [(name, root / name) for name in ("LICENSE", "NOTICE.md")]
|
|
files += [(f"{path.name}/{file.relative_to(path).as_posix()}", file) for file in sorted(path.rglob("*")) if file.is_file()]
|
|
for name, file in files:
|
|
info = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0))
|
|
info.external_attr = 0o100644 << 16
|
|
archive.writestr(info, file.read_bytes())
|
|
return hashlib.sha256(destination.read_bytes()).hexdigest()
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("action", choices=("check", "pack"))
|
|
parser.add_argument("path", type=Path)
|
|
parser.add_argument("destination", type=Path, nargs="?")
|
|
args = parser.parse_args()
|
|
if args.action == "pack":
|
|
if args.destination is None:
|
|
parser.error("pack requires a destination")
|
|
print(package_archive(args.path, args.destination))
|
|
else:
|
|
print(check_package(args.path))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|