fix: declare Hermes auto image support and migrate running accounts

This commit is contained in:
snowspeeder committed 2026-10-02 16:17:29 -04:00
1 parent 6b8d55fc92
commit 437745bcca
4 files changed
+189 -4

No files matched your search

+60 -3
View File
@@ -252,6 +252,40 @@ class Provisioner:
raise ProvisioningError('Invalid persisted Hermes API credential')
return key
@staticmethod
def _declare_auto_vision(value):
"""Patch only the verified managed auto model, preserving other metadata."""
providers = value.get('providers', {})
if not isinstance(providers, dict):
raise ValueError('Invalid provider configuration')
provider = providers.get('hope-upstream')
if provider is None:
return False
if not isinstance(provider, dict):
raise ValueError('Invalid managed provider')
models = provider.get('models', {})
if isinstance(models, list):
if not all(isinstance(model, str) for model in models):
raise ValueError('Invalid model list')
if 'auto' not in models:
return False
models = {model: {} for model in models}
provider['models'] = models
if not isinstance(models, dict):
raise ValueError('Invalid model metadata')
if 'auto' not in models:
return False
metadata = models['auto']
if not isinstance(metadata, dict):
raise ValueError('Invalid auto metadata')
if metadata.get('supports_vision') is True:
return False
metadata['supports_vision'] = True
return True
def _reload_marker(self, user):
return self._paths(user)[0] / 'config-reload-pending'
def _prepare(self, user, exists, port):
private, home, workspace = self._paths(user)
self._directory(self.state_dir)
@@ -270,7 +304,7 @@ class Provisioner:
elif config.is_symlink():
raise ProvisioningError('Hermes config cannot be a symlink')
else:
# Preserve user configuration; only control-owned listener settings change.
# Preserve user configuration except managed listeners/verified auto capability.
try:
value = yaml.safe_load(config.read_text())
if not isinstance(value, dict):
@@ -278,8 +312,13 @@ class Provisioner:
server = value.setdefault('gateway', {}).setdefault('api_server', {})
if not isinstance(server, dict):
raise ValueError()
changed = self._declare_auto_vision(value)
if server.get('host') != '127.0.0.1' or server.get('port') != port:
server.update(host='127.0.0.1', port=port)
changed = True
if changed:
# Durable before writing: retry must reload even after a crash.
self._write_private(self._reload_marker(user), 'pending\n')
self._write_private(config, yaml.safe_dump(value, sort_keys=False))
except (ValueError, TypeError, AttributeError, yaml.YAMLError) as exc:
raise ProvisioningError('Invalid Hermes runtime configuration') from exc
@@ -298,7 +337,7 @@ class Provisioner:
'model': {'default': model, 'provider': 'custom:hope-upstream'},
'providers': {'hope-upstream': {'api': os.environ.get('HERMES_UPSTREAM_URL', 'http://192.168.86.5:2099/v1'),
'key_env': 'HERMES_UPSTREAM_API_KEY', 'transport': 'codex_responses', 'default_model': model,
'models': [model], 'discover_models': False}},
'models': {'auto': {'supports_vision': True}} if model == 'auto' else [model], 'discover_models': False}},
'agent': {'reasoning_effort': 'none'},
'terminal': {'backend': 'local', 'cwd': '/workspace'},
'platform_toolsets': {'api_server': ['terminal', 'file', 'web', 'browser', 'memory', 'skills', 'delegation']},
@@ -422,8 +461,13 @@ class Provisioner:
# Never start a host-networked container with an unexpected listener.
self._connection(user, info, key)
await self._remove_legacy_network(user)
marker = self._reload_marker(user)
if marker.is_symlink():
raise ProvisioningError('Reload marker cannot be a symlink')
if not info.get('State', {}).get('Running'):
await self._request('POST', f'/containers/{info["Id"]}/start', allowed=(204, 304))
elif marker.exists():
await self._request('POST', f'/containers/{info["Id"]}/restart', allowed=(204,))
# One overall deadline also bounds Docker inspect / health I/O; no
# deletion on failed health, so a retry starts/adopts the same state.
try:
@@ -436,6 +480,10 @@ class Provisioner:
if info.get('State', {}).get('Running'):
connection = self._connection(user, info, key)
if await self._healthy(connection):
if marker.exists():
if marker.is_symlink() or marker.resolve() != self._paths(user)[0].resolve() / 'config-reload-pending':
raise ProvisioningError('Invalid reload marker path')
marker.unlink()
return connection
await asyncio.sleep(self.poll_interval)
except TimeoutError as exc:
@@ -446,9 +494,18 @@ class Provisioner:
info = await self._inspect(container_name(user))
if not info or not info.get('State', {}).get('Running'):
return False
marker = self._reload_marker(user)
if marker.exists() or marker.is_symlink():
return False
config = self._paths(user)[1] / 'config.yaml'
if config.is_symlink():
return False
value = yaml.safe_load(config.read_text())
if not isinstance(value, dict) or self._declare_auto_vision(value):
return False
key = self._read_key(user)
return bool(key) and await self._healthy(self._connection(user, info, key))
except (ProvisioningError, ValueError, OSError):
except (ProvisioningError, ValueError, OSError, yaml.YAMLError):
return False
async def remove(self, user: dict):