fix: declare Hermes auto image support and migrate running accounts
This commit is contained in:
1 parent
6b8d55fc92
commit
437745bcca
4 files changed
+189
-4
No files matched your search
+60
-3
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user