"""Export current workshop instructions and code as inert bytes, never credentials.""" import sys sys.dont_write_bytecode = True import argparse import datetime import hashlib import json import os from pathlib import Path, PurePosixPath import re import zipfile SHA = lambda raw: hashlib.sha256(raw).hexdigest() ROOT_FILES = {'SKILL.md', 'FILES.sha256', 'runtime-lock.json'} def permitted(name): parts = PurePosixPath(name).parts if not parts or '..' in parts or name.startswith('/') or '\\' in name or ':' in name: return False if name in ROOT_FILES: return True # Instance binding, envelopes, key material, environment and employee state # are outside these explicit documentation/code namespaces. if any(re.search(r'credential|binding|envelope|private|secret|recipient|decrypt|\.env', p, re.I) for p in parts): return False if parts[0] == 'references': return name.endswith('.md') or name == 'references/business-data-dictionary.json' return parts[0] == 'scripts' and Path(name).suffix in ('.py', '.mjs', '.js') def export(skill_root, out): root = Path(skill_root).absolute() target = Path(out).absolute() for path in (root, target): if any(p.is_symlink() for p in (path, *path.parents)): raise ValueError('REDIRECTED_PATH') if not root.is_dir() or not (root / 'SKILL.md').is_file(): raise ValueError('WORKSHOP_SKILL_NOT_FOUND') if target.exists() or root == target or root in target.parents: raise ValueError('NEW_OUTPUT_OUTSIDE_SKILL_REQUIRED') files = {} for path in root.rglob('*'): name = path.relative_to(root).as_posix() if not permitted(name): continue if any(p.is_symlink() for p in (path, *path.parents)): raise ValueError('REDIRECTED_SKILL_FILE') if not path.is_file(): continue if path.stat().st_size > 8 * 1024 * 1024: raise ValueError('SKILL_FILE_TOO_LARGE') files[name] = path.read_bytes() if len(files) > 100 or sum(map(len, files.values())) > 20 * 1024 * 1024: raise ValueError('SKILL_EXPORT_TOO_LARGE') manifest_state = [] manifest = files.get('FILES.sha256') if manifest: for line in manifest.decode('utf-8-sig').splitlines(): digest, sep, name = line.partition(' ') if not sep or not re.fullmatch('[a-f0-9]{64}', digest): raise ValueError('INVALID_INSTALLED_MANIFEST') manifest_state.append({'path': name, 'state': ('MATCH' if SHA(files[name]) == digest else 'MISMATCH') if name in files else 'NOT_EXPORTED'}) # Re-read the public files before sealing; do not copy during an upgrade. if any((root / name).read_bytes() != raw for name, raw in files.items()): raise ValueError('SKILL_CHANGED_DURING_EXPORT') receipt = {'schema': 'ksf.workshop.skill-readonly-export.v1', 'exportedAt': datetime.datetime.now(datetime.timezone.utc).isoformat(), 'skillName': 'ksf-workshop-runtime', 'skillSha256': SHA(files['SKILL.md']), 'skillLines': len(files['SKILL.md'].decode('utf-8-sig').splitlines()), 'files': [{'path': n, 'bytes': len(b), 'sha256': SHA(b)} for n, b in sorted(files.items())], 'installedManifest': manifest_state, 'readOnly': True, 'installedFilesModified': False, 'exportFilesWritten': True, 'packageCodeExecuted': False, 'credentialsExported': False, 'databaseWrites': 0, 'networkCalls': 0, 'paidCalls': 0, 'agentReloadVerified': False, 'actualBusinessWriteVerified': False} target.mkdir(parents=True, mode=0o700) archive = target / 'ksf-workshop-runtime-current.zip' payload = {'ksf-workshop-runtime/' + n: b for n, b in files.items()} payload['EXPORT.json'] = (json.dumps(receipt, ensure_ascii=False, indent=2) + '\n').encode('utf-8') payload['EXPORT.sha256'] = ''.join(SHA(b) + ' ' + n + '\n' for n, b in sorted(payload.items())).encode('utf-8') with zipfile.ZipFile(archive, 'x', zipfile.ZIP_DEFLATED) as z: for name, raw in payload.items(): z.writestr(name, raw) os.chmod(archive, 0o600) with zipfile.ZipFile(archive) as z: if z.testzip() is not None or any(z.read(n) != b for n, b in payload.items()): raise ValueError('EXPORT_VERIFICATION_FAILED') result = {'ok': True, 'archive': str(archive), 'bytes': archive.stat().st_size, 'sha256': SHA(archive.read_bytes()), 'skillSha256': receipt['skillSha256'], 'skillLines': receipt['skillLines'], 'files': len(files), 'installedManifestMismatches': [r['path'] for r in manifest_state if r['state'] == 'MISMATCH'], 'installedFilesModified': False, 'databaseWrites': 0, 'paidCalls': 0} (target / 'export-receipt.json').write_text(json.dumps(result, ensure_ascii=False, indent=2) + '\n', encoding='utf-8') os.chmod(target / 'export-receipt.json', 0o600) return result if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--skill-root', default='/opt/data/skills/ksf-workshop-runtime') parser.add_argument('--out', required=True) args = parser.parse_args() try: print(json.dumps(export(args.skill_root, args.out), ensure_ascii=False)) except (ValueError, OSError, KeyError) as error: code = str(error) if re.fullmatch('[A-Z][A-Z0-9_]+', str(error)) else 'EXPORT_FAILED' print(json.dumps({'ok': False, 'error': {'code': code}, 'installedFilesModified': False}, ensure_ascii=False)) sys.exit(1)