mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-29 20:05:25 +08:00
171 lines
7.7 KiB
Python
171 lines
7.7 KiB
Python
"""Resolve official published regression plugins once, then install the recorded bytes."""
|
|
|
|
from datetime import datetime, timezone
|
|
import hashlib
|
|
import io
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import urllib.error
|
|
import urllib.request
|
|
import zipfile
|
|
|
|
import yaml
|
|
|
|
|
|
MARKETPLACE = 'https://space.langbot.app'
|
|
PLUGIN_IDS = ('langbot-team/LocalAgent', 'langbot-team/LangRAG', 'langbot-team/GeneralParsers')
|
|
METADATA_LIMIT = 1024 * 1024
|
|
PACKAGE_LIMIT = 64 * 1024 * 1024
|
|
|
|
|
|
def write_manifest(directory, manifest):
|
|
path = directory / 'manifest.json'
|
|
temporary = directory / 'manifest.json.tmp'
|
|
temporary.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + '\n')
|
|
temporary.replace(path)
|
|
|
|
|
|
def fetch(url, limit):
|
|
request = urllib.request.Request(url, headers={'User-Agent': 'LangBot-regression/1'})
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=60) as response:
|
|
if response.status != 200:
|
|
raise RuntimeError(f'Marketplace request failed: HTTP {response.status}: {url}')
|
|
content = response.read(limit + 1)
|
|
except (urllib.error.URLError, OSError) as exc:
|
|
raise RuntimeError(f'Marketplace request failed: {url}: {exc}') from exc
|
|
if len(content) > limit:
|
|
raise ValueError(f'Marketplace response exceeds {limit} bytes: {url}')
|
|
return content
|
|
|
|
|
|
def release_version(value):
|
|
if not isinstance(value, str) or not re.fullmatch(r'[a-zA-Z0-9][a-zA-Z0-9._+-]{0,127}', value):
|
|
raise ValueError(f'Invalid published plugin version: {value!r}')
|
|
return value
|
|
|
|
|
|
def replay_versions(path):
|
|
manifest = json.loads(Path(path).read_text())
|
|
if not isinstance(manifest, dict) or manifest.get('version') != 1 or manifest.get('marketplace') != MARKETPLACE:
|
|
raise ValueError('Replay requires a version 1 official marketplace manifest')
|
|
entries = manifest.get('plugins')
|
|
if not isinstance(entries, list) or len(entries) != len(PLUGIN_IDS):
|
|
raise ValueError('Replay manifest must contain the three regression plugins')
|
|
selected = {}
|
|
for entry in entries:
|
|
if not isinstance(entry, dict) or entry.get('id') not in PLUGIN_IDS or entry['id'] in selected:
|
|
raise ValueError('Replay manifest has an unknown or duplicate plugin')
|
|
version = release_version(entry.get('version'))
|
|
digest = entry.get('sha256')
|
|
if not isinstance(digest, str) or not re.fullmatch('[0-9a-f]{64}', digest):
|
|
raise ValueError('Replay manifest requires a package sha256 for every plugin')
|
|
selected[entry['id']] = (version, digest)
|
|
return selected
|
|
|
|
|
|
def check_archive(content, plugin_id, version):
|
|
try:
|
|
with zipfile.ZipFile(io.BytesIO(content)) as archive:
|
|
if archive.namelist().count('manifest.yaml') != 1:
|
|
raise ValueError('package needs exactly one root manifest.yaml')
|
|
info = archive.getinfo('manifest.yaml')
|
|
if info.file_size > METADATA_LIMIT:
|
|
raise ValueError('package manifest exceeds the metadata limit')
|
|
manifest = yaml.safe_load(archive.read(info))
|
|
metadata = manifest.get('metadata') if isinstance(manifest, dict) else None
|
|
if (
|
|
not isinstance(metadata, dict)
|
|
or manifest.get('kind') != 'Plugin'
|
|
or f'{metadata.get("author")}/{metadata.get("name")}' != plugin_id
|
|
or metadata.get('version') != version
|
|
):
|
|
raise ValueError('package manifest identity/version differs from the selected release')
|
|
except (zipfile.BadZipFile, yaml.YAMLError, KeyError, ValueError, RuntimeError) as exc:
|
|
raise ValueError(f'Invalid published package for {plugin_id}@{version}: {exc}') from exc
|
|
|
|
|
|
def prepare_published_plugins(directory, replay=None):
|
|
"""Download releases; replay reads only identities, versions and hashes, never supplied URLs."""
|
|
directory = Path(directory)
|
|
directory.mkdir(parents=True, exist_ok=False)
|
|
selected = replay_versions(replay) if replay is not None else None
|
|
manifest = {
|
|
'version': 1,
|
|
'marketplace': MARKETPLACE,
|
|
'resolved_at': datetime.now(timezone.utc).isoformat(),
|
|
'selection': 'replay' if selected is not None else 'latest',
|
|
'plugins': [],
|
|
}
|
|
write_manifest(directory, manifest)
|
|
for plugin_id in PLUGIN_IDS:
|
|
metadata_url = f'{MARKETPLACE}/api/v1/marketplace/plugins/{plugin_id}'
|
|
if selected is None:
|
|
payload = json.loads(fetch(metadata_url, METADATA_LIMIT))
|
|
data = payload.get('data') if isinstance(payload, dict) else None
|
|
plugin = data.get('plugin') if isinstance(data, dict) else None
|
|
if (
|
|
not isinstance(payload, dict)
|
|
or payload.get('code') != 0
|
|
or not isinstance(plugin, dict)
|
|
or plugin.get('plugin_id') != plugin_id
|
|
or f'{plugin.get("author")}/{plugin.get("name")}' != plugin_id
|
|
or plugin.get('status') != 'live'
|
|
or plugin.get('deleted_at') is not None
|
|
):
|
|
raise ValueError(f'Marketplace did not return a live published plugin: {plugin_id}')
|
|
version = release_version(plugin.get('latest_version'))
|
|
else:
|
|
version = selected[plugin_id][0]
|
|
manifest['plugins'].append(
|
|
{
|
|
'id': plugin_id,
|
|
'version': version,
|
|
'metadata_url': metadata_url,
|
|
'package_url': f'{MARKETPLACE}/api/v1/marketplace/plugins/download/{plugin_id}/{version}',
|
|
'archive': f'{plugin_id.split("/")[1]}-{version}.zip',
|
|
}
|
|
)
|
|
write_manifest(directory, manifest)
|
|
|
|
for entry in manifest['plugins']:
|
|
content = fetch(entry['package_url'], PACKAGE_LIMIT)
|
|
digest = hashlib.sha256(content).hexdigest()
|
|
if selected is not None and digest != selected[entry['id']][1]:
|
|
raise ValueError(f'Replay package sha256 changed: {entry["id"]}@{entry["version"]}')
|
|
check_archive(content, entry['id'], entry['version'])
|
|
(directory / entry['archive']).write_bytes(content)
|
|
entry['sha256'] = digest
|
|
write_manifest(directory, manifest)
|
|
return manifest
|
|
|
|
|
|
def install_published_plugins(call, directory, manifest):
|
|
"""Install the downloaded packages and require exact, initialized CLI readback."""
|
|
directory = Path(directory)
|
|
for entry in manifest['plugins']:
|
|
archive = directory / entry['archive']
|
|
if hashlib.sha256(archive.read_bytes()).hexdigest() != entry['sha256']:
|
|
raise ValueError(f'Plugin archive changed before installation: {entry["id"]}')
|
|
try:
|
|
call(['plugin', 'install', 'local', '--file', str(archive), '--wait'])
|
|
except RuntimeError as exc:
|
|
raise RuntimeError(f'Published plugin install failed: {entry["id"]}@{entry["version"]}: {exc}') from exc
|
|
response = call(['plugin', 'list'])
|
|
plugins = response.get('plugins') if isinstance(response, dict) else None
|
|
if not isinstance(plugins, list) or any(not isinstance(item, dict) for item in plugins):
|
|
raise RuntimeError('Plugin readback did not return a plugin list')
|
|
matches = [item for item in plugins if f'{item.get("author")}/{item.get("name")}' == entry['id']]
|
|
entry['installed'] = [{key: item.get(key) for key in ('version', 'enabled', 'status')} for item in matches]
|
|
write_manifest(directory, manifest)
|
|
if (
|
|
len(matches) != 1
|
|
or matches[0].get('version') != entry['version']
|
|
or matches[0].get('enabled') is not True
|
|
or matches[0].get('status') != 'initialized'
|
|
):
|
|
raise RuntimeError(
|
|
f'Published plugin is not ready at the selected version: {entry["id"]}: {entry["installed"]}'
|
|
)
|