feat: add Hermes WebUI provisioning and English Dragon HD Omni voices
Rebuild & Deploy / rebuild (push) Successful in 2m17s

This commit is contained in:
snowspeeder committed 2026-10-02 16:07:24 -04:00
1 parent 47cc9c88a0
commit 6b8d55fc92
59 files changed
+1175 -70

No files matched your search

+147 -6
View File
@@ -6,6 +6,8 @@ import os
import httpx
import pytest
import yaml
import socket
from control.provisioner import OwnershipError, Provisioner, ProvisioningError, container_name
@@ -39,6 +41,7 @@ class DockerBoundary:
spec = json.loads(request.content)
self.created.append(spec)
self.containers[name] = {'Id': 'id-' + name, 'Config': copy.deepcopy(spec),
'HostConfig': copy.deepcopy(spec['HostConfig']),
'State': {'Running': False}, 'NetworkSettings': {'Ports': {}}}
return httpx.Response(201, json={'Id': 'id-' + name})
key = path.split('/')[2]
@@ -99,11 +102,12 @@ def test_account_isolation_and_runtime_configuration(boundary):
assert env['HERMES_UPSTREAM_API_KEY'] == 'runtime-provider-secret'
assert env['API_SERVER_KEY'] == connection['hermes_api_key']
assert env['API_SERVER_ENABLED'] == 'true'
assert env['API_SERVER_HOST'] == '0.0.0.0'
assert env['API_SERVER_PORT'] == '8642'
assert env['API_SERVER_HOST'] == '127.0.0.1'
assert int(env['API_SERVER_PORT']) == int(connection['hermes_url'].rsplit(':', 1)[1])
host = spec['HostConfig']
assert host['NetworkMode'] == 'hope-hermes-net-' + user['id']
assert host['PortBindings']['8642/tcp'] == [{'HostIp': '127.0.0.1', 'HostPort': ''}]
assert host['NetworkMode'] == 'host'
assert 'PortBindings' not in host
assert 'ExposedPorts' not in spec
assert host['Memory'] > 0 and host['NanoCpus'] > 0 and host['PidsLimit'] > 0
assert host['Privileged'] is False and host['Init'] is True
assert host['CapDrop'] == ['ALL']
@@ -143,7 +147,8 @@ def test_retries_restart_reuse_key_and_preserve_home(boundary):
restarted = Provisioner(p.state_dir, docker_client=p.docker, health_client=p.health,
upstream_api_key='runtime-provider-secret', runtime_uid=p.runtime_uid, runtime_gid=p.runtime_gid)
assert asyncio.run(restarted.ensure(ALICE)) == result
assert json.loads((home / 'config.yaml').read_text()) == {'retained': True}
assert yaml.safe_load((home / 'config.yaml').read_text()) == {
'retained': True, 'gateway': {'api_server': {'host': '127.0.0.1', 'port': p._ports()[ALICE['id']]}}}
asyncio.run(restarted.remove(ALICE))
asyncio.run(restarted.remove(ALICE))
assert docker.deleted == [('id-' + container_name(ALICE), {'force': 'true', 'v': 'false'})]
@@ -185,7 +190,8 @@ def test_untrusted_paths_and_public_bind_fail_closed(boundary):
asyncio.run(p.ensure(user))
asyncio.run(p.ensure(ALICE))
info = docker.containers[container_name(ALICE)]
info['NetworkSettings']['Ports']['8642/tcp'][0]['HostIp'] = '0.0.0.0'
info['Config']['Env'] = [e.replace('API_SERVER_HOST=127.0.0.1', 'API_SERVER_HOST=0.0.0.0')
for e in info['Config']['Env']]
assert not asyncio.run(p.ready(ALICE))
with pytest.raises(ProvisioningError, match='loopback'):
asyncio.run(p.ensure(ALICE))
@@ -212,3 +218,138 @@ def test_upstream_runtime_overrides(monkeypatch):
assert provider['api'] == 'http://127.0.0.1:9999/v1'
assert provider['models'] == ['served-model']
assert provider['default_model'] == 'served-model'
def test_migrate_legacy_container_preserves_state_and_key(boundary):
p, docker, _ = boundary
old = asyncio.run(p.ensure(ALICE))
_, home, workspace = p._paths(ALICE)
(workspace / 'retained.txt').write_text('account work')
(home / 'config.yaml').write_text('custom: retained\ngateway:\n api_server:\n host: 0.0.0.0\n port: 8642\n')
info = docker.containers[container_name(ALICE)]
info['HostConfig']['NetworkMode'] = 'hope-hermes-net-' + ALICE['id']
net = info['HostConfig']['NetworkMode']
docker.networks[net] = {'Id': net, 'Name': net, 'Labels': info['Config']['Labels']}
assert not asyncio.run(p.ready(ALICE))
result = asyncio.run(p.ensure(ALICE))
assert result == old
assert len(docker.created) == 2 and len(docker.deleted) == 1
assert not docker.networks
assert (workspace / 'retained.txt').read_text() == 'account work'
config = yaml.safe_load((home / 'config.yaml').read_text())
assert config['custom'] == 'retained'
assert config['gateway']['api_server'] == {'host': '127.0.0.1', 'port': p._ports()[ALICE['id']]}
asyncio.run(p.ensure(ALICE))
assert len(docker.created) == 2
def test_ports_unique_concurrent_persisted_and_skip_busy_port(boundary):
p, docker, _ = boundary
with socket.socket() as occupied:
occupied.bind(('127.0.0.1', 0))
start = occupied.getsockname()[1]
p.api_port_range = (start, min(start + 10, 65535))
async def create():
return await asyncio.gather(p.ensure(ALICE), p.ensure(BOB))
alice, bob = asyncio.run(create())
ports = p._ports()
assert ports[ALICE['id']] != start
assert ports[ALICE['id']] != ports[BOB['id']]
assert (p.state_dir / 'api-ports.json').stat().st_mode & 0o777 == 0o600
restarted = Provisioner(p.state_dir, docker_client=p.docker, health_client=p.health,
upstream_api_key='runtime-provider-secret', runtime_uid=p.runtime_uid, runtime_gid=p.runtime_gid)
assert asyncio.run(restarted.ensure(ALICE)) == alice
def test_port_exhaustion_and_corrupt_registry_fail_closed(boundary):
p, docker, _ = boundary
with socket.socket() as occupied:
occupied.bind(('127.0.0.1', 0))
p.api_port_range = (occupied.getsockname()[1], occupied.getsockname()[1])
with pytest.raises(ProvisioningError, match='No available'):
asyncio.run(p.ensure(ALICE))
p._directory(p.state_dir)
p._write_private(p.state_dir / 'api-ports.json', json.dumps({ALICE['id']: 20000, BOB['id']: 20000}))
with pytest.raises(ProvisioningError, match='Duplicate'):
asyncio.run(p.ensure(ALICE))
assert not docker.created
def test_migration_start_failure_retries_without_losing_data(boundary):
p, docker, _ = boundary
result = asyncio.run(p.ensure(ALICE))
docker.containers[container_name(ALICE)]['HostConfig']['NetworkMode'] = 'legacy'
docker.fail_start = True
with pytest.raises(ProvisioningError, match='HTTP 500'):
asyncio.run(p.ensure(ALICE))
assert asyncio.run(p.ensure(ALICE)) == result
assert len(docker.created) == 2
def test_webui_migration_unique_ports_passwords_and_cookies(boundary, monkeypatch):
p, docker, _ = boundary
original = asyncio.run(p.ensure(ALICE))
p.webui_enabled = True
p.webui_host = '127.0.0.1'
p.image = 'hope-hermes-webui:62e4d7b'
async def healthy(connection):
return True
monkeypatch.setattr(p, '_healthy', healthy)
alice, bob = asyncio.run(p.ensure(ALICE)), asyncio.run(p.ensure(BOB))
assert alice == original
assert len(docker.created) == 3
a = dict(e.split('=', 1) for e in docker.created[1]['Env'])
b = dict(e.split('=', 1) for e in docker.created[2]['Env'])
assert a['HERMES_WEBUI_PORT'] != b['HERMES_WEBUI_PORT']
assert a['HERMES_WEBUI_PASSWORD'] != b['HERMES_WEBUI_PASSWORD']
assert a['HERMES_WEBUI_COOKIE_NAME'] != b['HERMES_WEBUI_COOKIE_NAME']
assert a['HERMES_WEBUI_PASSWORD'] != alice['hermes_api_key']
assert a['HERMES_WEBUI_GATEWAY_API_KEY'] == alice['hermes_api_key']
assert a['HERMES_WEBUI_GATEWAY_BASE_URL'] == alice['hermes_url']
assert a['HERMES_WEBUI_SECURE'] == '1'
assert a['HERMES_WEBUI_CHAT_BACKEND'] == 'gateway'
password = (p.state_dir / ALICE['id'] / 'webui-password')
assert password.stat().st_mode & 0o777 == 0o600
asyncio.run(p.ensure(ALICE))
assert len(docker.created) == 3
def test_webui_password_symlink_fails_closed(boundary, monkeypatch):
p, docker, _ = boundary
asyncio.run(p.ensure(ALICE))
private, _, _ = p._paths(ALICE)
(private / 'webui-password').symlink_to(private / 'api-key')
p.webui_enabled = True
p.webui_host = '127.0.0.1'
with pytest.raises(ProvisioningError, match='symlink'):
asyncio.run(p.ensure(ALICE))
assert len(docker.created) == 1
@pytest.mark.parametrize('password', ['x', 'short passphrase'])
def test_webui_accepts_user_chosen_short_password(boundary, monkeypatch, password):
p, docker, _ = boundary
asyncio.run(p.ensure(ALICE))
private, _, _ = p._paths(ALICE)
(private / 'webui-password').write_text(password + '\n')
p.webui_enabled = True
p.webui_host = '127.0.0.1'
async def healthy(connection):
return True
monkeypatch.setattr(p, '_healthy', healthy)
asyncio.run(p.ensure(ALICE))
env = dict(entry.split('=', 1) for entry in docker.created[-1]['Env'])
assert env['HERMES_WEBUI_PASSWORD'] == password
@pytest.mark.parametrize('password', ['', ' ', 'first\nsecond'])
def test_webui_rejects_blank_or_multiline_password_before_recreation(boundary, password):
p, docker, _ = boundary
asyncio.run(p.ensure(ALICE))
private, _, _ = p._paths(ALICE)
(private / 'webui-password').write_text(password)
p.webui_enabled = True
p.webui_host = '127.0.0.1'
with pytest.raises(ProvisioningError, match='nonempty and single-line'):
asyncio.run(p.ensure(ALICE))
assert not docker.deleted