SkillScreen-2B-Package / scripts /skill_package.py
JerroldH's picture
Release v0.1.0: full-data deployment model, inference entry point and OOF thresholds
4fa3363 verified
Raw History Blame Contribute Delete
4.7 kB
"""Read a skill as inert text; never import, install, or execute its files."""
import hashlib
import json
import os
from pathlib import Path
import stat
IGNORE_DIRS = {'.git', '.venv', 'venv', 'node_modules', '__pycache__', '.ssh', '.aws'}
MAX_FILE_BYTES = 1_048_576
MAX_PACKAGE_BYTES = 16_777_216
MAX_ENTRIES = 1000
def digest(value):
return hashlib.sha256(value).hexdigest()
def read_package(path):
path = Path(path).absolute()
if path.is_symlink() or path.is_junction():
raise ValueError('Choose a real package directory or SKILL.md, not a link.')
single = path.is_file()
if single and path.name != 'SKILL.md':
raise ValueError('A file input must be named SKILL.md.')
root = path.parent if single else path
if not root.is_dir():
raise ValueError('Package directory does not exist.')
root = root.resolve()
files, skipped, total, entries = [], [], 0, 0
if single:
candidates = [(root, [], ['SKILL.md'])]
else:
candidates = os.walk(root, followlinks=False, onerror=lambda exc: (_ for _ in ()).throw(exc))
for directory, dirs, names in candidates:
directory = Path(directory)
entries += len(dirs) + len(names)
if entries > MAX_ENTRIES:
raise ValueError(f'Package exceeds {MAX_ENTRIES} entries; select a smaller skill directory.')
for name in sorted(dirs[:]):
item = directory / name
reason = ('excluded_directory' if name.lower() in IGNORE_DIRS else
'link' if item.is_symlink() or item.is_junction() else None)
if reason:
dirs.remove(name)
skipped.append({'path': item.relative_to(root).as_posix() + '/', 'reason': reason})
dirs.sort()
for name in sorted(names):
item = directory / name
relative = item.relative_to(root).as_posix()
reason = None
if item.is_symlink() or item.is_junction():
reason = 'link'
elif name.lower() == '.env' or name.lower().startswith('.env.') or item.suffix.lower() in {'.pem', '.key'}:
reason = 'private_configuration'
elif not stat.S_ISREG(item.stat().st_mode):
reason = 'not_regular_file'
elif any(ord(char) < 32 or ord(char) == 127 for char in relative) or '\\' in relative:
reason = 'unsupported_path'
elif not item.resolve().is_relative_to(root):
raise ValueError('A file resolved outside the package.')
if reason:
skipped.append({'path': relative, 'reason': reason})
continue
with item.open('rb') as stream:
raw = stream.read(MAX_FILE_BYTES + 1)
if len(raw) > MAX_FILE_BYTES:
skipped.append({'path': relative, 'reason': 'file_size_limit'})
continue
total += len(raw)
if total > MAX_PACKAGE_BYTES:
raise ValueError('Package exceeds the 16 MiB text-read budget.')
try:
text = raw.decode('utf-8')
if '\x00' in text:
raise UnicodeError('NUL byte')
except UnicodeError:
skipped.append({'path': relative, 'reason': 'not_utf8_text'})
continue
files.append({'path': relative, 'sha256': digest(raw), 'bytes': len(raw), 'text': text})
files.sort(key=lambda item: item['path'])
markdown = next((item for item in files if item['path'] == 'SKILL.md'), None)
if markdown is None or not markdown['text'].strip():
raise ValueError('A nonempty UTF-8 SKILL.md is required at the package root.')
manifest = [{key: item[key] for key in ('path', 'sha256', 'bytes')} for item in files]
fingerprint = digest(json.dumps(manifest, sort_keys=True, separators=(',', ':')).encode())
row = {'skill_text': markdown['text'],
'files': [{'path': item['path'], 'text': item['text']} for item in files if item is not markdown]}
coverage = {'input_kind': 'markdown_file' if single else 'package_directory',
'text_manifest_sha256': fingerprint, 'text_files': len(files),
'text_bytes': sum(item['bytes'] for item in files), 'files': manifest, 'skipped': skipped}
return row, coverage
def write_json(path, value):
"""Create outputs explicitly; do not replace a user's existing file."""
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
with path.open('x', encoding='utf-8', newline='\n') as stream:
stream.write(json.dumps(value, indent=2, ensure_ascii=False, allow_nan=False) + '\n')