File size: 4,699 Bytes
b60475f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
"""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')