From be87628d673ee59775316708a96a66d02719c553 Mon Sep 17 00:00:00 2001 From: Julian lechner Date: Thu, 17 Sep 2026 08:32:17 +0200 Subject: [PATCH] feat(plugins): record resolved extension revisions --- scripts/lib/managed-extensions.py | 37 +++++++++++++++++++++++++++--- scripts/test-managed-extensions.py | 7 ++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/scripts/lib/managed-extensions.py b/scripts/lib/managed-extensions.py index 10f0c21..fe67c73 100644 --- a/scripts/lib/managed-extensions.py +++ b/scripts/lib/managed-extensions.py @@ -137,6 +137,17 @@ def fetch_skill(entry): return skill, files +def resolve_revision(source): + try: + request = urllib.request.Request(f'https://api.github.com/repos/{source}/commits/HEAD', headers={'Accept': 'application/vnd.github+json', 'User-Agent': 'MyAiConfiguration'}) + with urllib.request.urlopen(request, timeout=10) as response: + payload = json.loads(response.read(1024 * 1024)) + revision = payload.get('sha') if isinstance(payload, dict) else None + return revision if isinstance(revision, str) and re.fullmatch(r'[0-9a-fA-F]{7,64}', revision) else None + except (OSError, ValueError, json.JSONDecodeError): + return None + + class Manager: def __init__(self, home, dry_run=False, update=False, summary=False): self.home = Path(home).absolute() @@ -166,6 +177,8 @@ class Manager: if resource['kind'] == 'plugin': if not re.fullmatch(r'[A-Za-z0-9_.-]+@[A-Za-z0-9_.-]+', resource.get('selector', '')): raise ValueError('Invalid owned plugin selector.') + if 'source' in resource and not isinstance(resource['source'], str): + raise ValueError('Invalid owned plugin source.') else: directory = resource.get('directory', '') if not re.fullmatch(r'\.agents/skills/[A-Za-z0-9_-]+', directory) or resource['client'] != 'codex': @@ -173,6 +186,8 @@ class Manager: safe_path(self.home, directory) if not isinstance(resource.get('files'), dict): raise ValueError('Invalid owned skill file inventory.') + if 'source' in resource and not isinstance(resource['source'], str): + raise ValueError('Invalid owned skill source.') for relative, value in resource['files'].items(): if not relative.startswith(directory + '/') or not re.fullmatch(r'[a-f0-9]{64}', value): raise ValueError('Invalid owned skill file record.') @@ -271,11 +286,17 @@ class Manager: if item is not None and not self.update: if client == 'claude' and item.get('enabled') is False: self.command([client, 'plugin', 'enable', selector]) + if record is not None and item.get('version') is not None: + record['version'] = item['version']; self.save() return if item is not None and client == 'claude': self.command([client, 'plugin', 'update', selector]) if item.get('enabled') is False: self.command([client, 'plugin', 'enable', selector]) + if record is not None: + self.cache.pop(client, None) + version = self.installed(client).get(selector, {}).get('version') + if version is not None: record['version'] = version; self.save() return marketplace = entry[client + '_marketplace'] if marketplace not in ('', '-', 'openai-curated-remote'): @@ -287,8 +308,14 @@ class Manager: self.command([client, 'plugin', 'marketplace', 'add', marketplace]) arguments = [client, 'plugin', 'install', selector, '--scope', 'user'] if client == 'claude' else [client, 'plugin', 'add', selector] self.command(arguments) + resolved_version = None + if self.update: + self.cache.pop(client, None) + resolved_version = self.installed(client).get(selector, {}).get('version') if record is None: - self.state['resources'].append(dict(client=client, name=entry['name'], kind='plugin', selector=selector)) + record = dict(client=client, name=entry['name'], kind='plugin', selector=selector, source=entry.get(client + '_marketplace', '-')) + if resolved_version is not None: record['version'] = resolved_version + self.state['resources'].append(record) self.save() self.cache[client][selector] = {'scope': 'user'} @@ -311,12 +338,16 @@ class Manager: record = None if record and record.get('complete') and not self.update and all(safe_path(self.home, name).is_file() for name in record['files']) and record['files']: return - _, files = fetch_skill(entry) + fetched = fetch_skill(entry) + _, files = fetched[:2] + revision = fetched[2] if len(fetched) > 2 else resolve_revision(entry['codex_source']) if record is None: - record = dict(client='codex', name=entry['name'], kind='skill', directory=directory, files={}, complete=False) + record = dict(client='codex', name=entry['name'], kind='skill', directory=directory, source=entry['codex_source'], files={}, complete=False) self.state['resources'].append(record) self.save() record['complete'] = False + if revision: + record['resolvedRevision'] = revision self.save() for name, (content, executable) in files.items(): relative = directory + '/' + name diff --git a/scripts/test-managed-extensions.py b/scripts/test-managed-extensions.py index a74181f..8d1203e 100644 --- a/scripts/test-managed-extensions.py +++ b/scripts/test-managed-extensions.py @@ -110,6 +110,13 @@ class ManagedExtensionTests(unittest.TestCase): self.assertTrue((directory / 'foreign.txt').exists()) self.assertFalse((directory / 'references/old.md').exists()) + def test_skill_records_resolved_revision(self): + with patch.object(extensions, 'resolve_revision', return_value='a' * 40): + self.manager().sync([skill()], ['codex'], {'Humanizer'}) + record = self.manager().state['resources'][0] + self.assertEqual(record['source'], 'blader/humanizer') + self.assertEqual(record['resolvedRevision'], 'a' * 40) + def test_skill_partial_failure_records_each_successful_file(self): original_write = extensions.atomic_write def write(path, content):