The distributed worker pool stacks on exactly two open pull requests, in this order: PR 14 supplies the broker-only SCM boundary the mediators route through and the hermes-scm-boundary-v2 ConfigMap they mount, and PR 15 supplies the cli_lane_* decomposition -- including canonical_run_id and the eligibility predicate on claim_ready -- that the coordinator depends on. Neither can be dropped without breaking a fixed P0 boundary, so both are carried here as prerequisites and this branch must not merge before them. PR 16 (agent image release lane) and PR 19 (full-handoff acceptance harness) are NOT prerequisites and are deliberately absent, so reviewing this branch no longer means approving them. PR 14 and PR 15 conflict with each other in nine paths. Each is resolved to the resolution already reviewed on this branch at 4d4cf1bd.
283 lines
11 KiB
Python
283 lines
11 KiB
Python
"""Image generation and sandbox-boundary contracts for Hermes chat."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import sys
|
|
import time
|
|
from types import SimpleNamespace
|
|
|
|
import yaml
|
|
|
|
from testing.tests.test_hermes_chat_support import (
|
|
HERMES,
|
|
VAULT,
|
|
_documents,
|
|
)
|
|
|
|
|
|
def test_chat_image_generation_uses_private_owner_broker():
|
|
"""Family pods get image bytes without receiving the owner's OAuth file."""
|
|
configmap = _documents(HERMES / "chat-configmap.yaml")[0]
|
|
assert "shared desktop/Wolf lane" in configmap["data"]["SOUL.md"]
|
|
assert "local FLUX waits" in configmap["data"]["SOUL.md"]
|
|
assert "Use `image_generate_local`" in configmap["data"]["SOUL.md"]
|
|
assert "Use `image_generate_hosted`" in configmap["data"]["SOUL.md"]
|
|
assert "ComfyUI endpoint" in configmap["data"]["SOUL.md"]
|
|
assert "`MEDIA:` path" in configmap["data"]["SOUL.md"]
|
|
assert "`image_edit_latest`" in configmap["data"]["SOUL.md"]
|
|
assert (
|
|
"Preserve the most recently selected image lane" in configmap["data"]["SOUL.md"]
|
|
)
|
|
config = yaml.safe_load(configmap["data"]["config.yaml"])
|
|
assert config["image_gen"] == {
|
|
"provider": "atlas-broker",
|
|
"model": "atlas-image-auto-high",
|
|
}
|
|
assert config["plugins"]["enabled"] == ["atlas-broker", "auto-router"]
|
|
|
|
statefulset = _documents(HERMES / "chat-statefulset.yaml")[0]
|
|
pod = statefulset["spec"]["template"]["spec"]
|
|
hermes = next(item for item in pod["containers"] if item["name"] == "hermes")
|
|
mounts = {item["name"]: item for item in hermes["volumeMounts"]}
|
|
assert mounts["image-plugin"]["mountPath"] == (
|
|
"/opt/hermes/plugins/image_gen/atlas-broker"
|
|
)
|
|
assert mounts["runtime-access"]["mountPath"] == "/runtime-access"
|
|
assert "provider-auth" not in mounts
|
|
assert not any(
|
|
mount["name"] == "home" and "agent" in str(mount)
|
|
for mount in hermes["volumeMounts"]
|
|
)
|
|
env = {item["name"]: item["value"] for item in hermes["env"]}
|
|
assert env["HERMES_IMAGE_BROKER_URL"].startswith("http://hermes-image-broker.")
|
|
|
|
plugin = (HERMES / "plugins" / "image-gen-broker" / "__init__.py").read_text()
|
|
assert '"local": "flux-2-klein-4b-local"' in plugin
|
|
assert '"hosted": "gpt-image-2-high"' in plugin
|
|
assert 'name="image_generate_local"' in plugin
|
|
assert 'name="image_generate_hosted"' in plugin
|
|
assert '"name": "image_edit_latest"' in plugin
|
|
assert '"name": "image_edit_latest_local"' in plugin
|
|
assert '"name": "image_edit_latest_hosted"' in plugin
|
|
assert "def _latest_generated_image" in plugin
|
|
assert "newest MEDIA: path from the conversation" in plugin
|
|
assert 'candidate.upper().startswith("MEDIA:")' in plugin
|
|
assert "override=True" not in plugin
|
|
|
|
agent = _documents(HERMES / "agent-deployment.yaml")[0]
|
|
containers = agent["spec"]["template"]["spec"]["containers"]
|
|
broker = next(item for item in containers if item["name"] == "image-broker")
|
|
assert broker["ports"] == [
|
|
{"name": "image-broker", "containerPort": 9002, "protocol": "TCP"}
|
|
]
|
|
assert broker["securityContext"]["readOnlyRootFilesystem"] is True
|
|
assert broker["securityContext"]["runAsNonRoot"] is True
|
|
|
|
services = _documents(HERMES / "service.yaml")
|
|
service = next(
|
|
item for item in services if item["metadata"]["name"] == "hermes-image-broker"
|
|
)
|
|
assert service["spec"]["selector"] == {"app": "hermes-agent"}
|
|
|
|
oauth_store = _documents(HERMES / "oauth-session-store.yaml")
|
|
redis = next(item for item in oauth_store if item["kind"] == "Deployment")
|
|
assert redis["spec"]["strategy"]["type"] == "Recreate"
|
|
assert "--appendonly" in redis["spec"]["template"]["spec"]["containers"][0]["args"]
|
|
|
|
policies = _documents(HERMES / "networkpolicy.yaml")
|
|
agent_policy = next(
|
|
item
|
|
for item in policies
|
|
if item["metadata"]["name"] == "hermes-agent-isolation"
|
|
)
|
|
broker_ingress = next(
|
|
rule
|
|
for rule in agent_policy["spec"]["ingress"]
|
|
if {port["port"] for port in rule["ports"]} == {9002, 9003}
|
|
)
|
|
assert broker_ingress["from"][0]["podSelector"]["matchLabels"] == {
|
|
"app": "hermes-chat-tenant"
|
|
}
|
|
vault_policy = (VAULT / "scripts" / "vault_k8s_auth_configure.sh").read_text()
|
|
agent_role = vault_policy[
|
|
vault_policy.index('write_policy_and_role "hermes-agent"') : vault_policy.index(
|
|
"write_policy_and_role",
|
|
vault_policy.index('write_policy_and_role "hermes-agent"') + 1,
|
|
)
|
|
]
|
|
assert "hermes/developer-gitea" not in agent_role
|
|
assert 'write_policy_and_role "hermes-scm-broker" "hermes-scm"' in vault_policy
|
|
assert (
|
|
'write_policy_and_role "hermes-node-ssh" "hermes" '
|
|
'"hermes-node-ssh-access"' in vault_policy
|
|
)
|
|
|
|
|
|
def test_compact_image_edit_resolves_latest_tenant_artifact(tmp_path, monkeypatch):
|
|
"""Follow-up edits resolve the source server-side and keep tool JSON small."""
|
|
provider_module = SimpleNamespace(
|
|
DEFAULT_ASPECT_RATIO="square",
|
|
ImageGenProvider=object,
|
|
error_response=lambda **value: value,
|
|
normalize_reference_images=lambda value: value,
|
|
resolve_aspect_ratio=lambda value: value,
|
|
save_b64_image=lambda *_args, **_kwargs: tmp_path / "saved.png",
|
|
success_response=lambda **value: value,
|
|
)
|
|
monkeypatch.setitem(sys.modules, "agent", SimpleNamespace())
|
|
monkeypatch.setitem(sys.modules, "agent.image_gen_provider", provider_module)
|
|
spec = importlib.util.spec_from_file_location(
|
|
"hermes_image_plugin",
|
|
HERMES / "plugins" / "image-gen-broker" / "__init__.py",
|
|
)
|
|
assert spec and spec.loader
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
|
|
image_dir = tmp_path / "cache" / "images"
|
|
image_dir.mkdir(parents=True)
|
|
older = image_dir / "atlas_flux-old.png"
|
|
newest = image_dir / "atlas_gpt-image-new.png"
|
|
older.write_bytes(b"older")
|
|
newest.write_bytes(b"newest")
|
|
older.touch()
|
|
time.sleep(0.001)
|
|
newest.touch()
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
|
|
calls = []
|
|
monkeypatch.setattr(
|
|
module,
|
|
"_handle_image_generate",
|
|
lambda args, route: calls.append((args, route)) or "ok",
|
|
)
|
|
assert module._handle_hosted_edit({"prompt": "make it a clown"}) == "ok"
|
|
assert calls == [
|
|
(
|
|
{
|
|
"prompt": "make it a clown",
|
|
"image_url": str(newest.resolve()),
|
|
},
|
|
"hosted",
|
|
)
|
|
]
|
|
|
|
|
|
def test_chat_reasoning_uses_switchyard_without_owner_credentials():
|
|
"""Family pods use AUTO/manual routes without mounting owner credentials."""
|
|
configmap = _documents(HERMES / "chat-configmap.yaml")[0]
|
|
config = yaml.safe_load(configmap["data"]["config.yaml"])
|
|
assert config["model"] == {
|
|
"provider": "atlas-switchyard",
|
|
"default": "atlas/auto/fast",
|
|
"model": "atlas/auto/fast",
|
|
}
|
|
assert config["providers"]["atlas-switchyard"] == {
|
|
"name": "Automatic Router",
|
|
"api": "http://hermes-switchyard.hermes.svc.cluster.local:9005/v1",
|
|
"api_key": "atlas-switchyard",
|
|
"default_model": "atlas/auto/fast",
|
|
"transport": "chat_completions",
|
|
}
|
|
assert config["platforms"]["api_server"]["extra"]["model_routes"] == {
|
|
route: {"provider": "atlas-switchyard", "model": route}
|
|
for route in [
|
|
"atlas/auto/fast",
|
|
"atlas/auto/balanced",
|
|
"atlas/auto/deep",
|
|
"atlas/auto/maximum",
|
|
"atlas/manual/codex/luna",
|
|
"atlas/manual/codex/terra",
|
|
"atlas/manual/codex/sol",
|
|
"atlas/manual/claude/haiku",
|
|
"atlas/manual/claude/fable",
|
|
"atlas/manual/claude/sonnet",
|
|
"atlas/manual/claude/opus",
|
|
"atlas/manual/local/qwen-14b",
|
|
]
|
|
}
|
|
|
|
agent = _documents(HERMES / "agent-deployment.yaml")[0]
|
|
containers = agent["spec"]["template"]["spec"]["containers"]
|
|
broker = next(item for item in containers if item["name"] == "codex-broker")
|
|
assert broker["ports"] == [
|
|
{"name": "codex-broker", "containerPort": 9003, "protocol": "TCP"}
|
|
]
|
|
assert broker["securityContext"]["readOnlyRootFilesystem"] is True
|
|
assert broker["securityContext"]["runAsNonRoot"] is True
|
|
assert {item["name"]: item["value"] for item in broker["env"]}.items() >= {
|
|
"PYTHONPATH": "/opt/hermes",
|
|
"HERMES_CODEX_BROKER_LISTEN_PORT": "9003",
|
|
"HERMES_ROUTING_CATALOG_PATH": "/routing-catalog/catalog.json",
|
|
}.items()
|
|
|
|
services = _documents(HERMES / "service.yaml")
|
|
service = next(
|
|
item for item in services if item["metadata"]["name"] == "hermes-codex-broker"
|
|
)
|
|
assert service["spec"]["selector"] == {"app": "hermes-agent"}
|
|
assert service["spec"]["ports"] == [
|
|
{
|
|
"name": "http",
|
|
"port": 9003,
|
|
"targetPort": "codex-broker",
|
|
"protocol": "TCP",
|
|
}
|
|
]
|
|
|
|
statefulset = _documents(HERMES / "chat-statefulset.yaml")[0]
|
|
assert (
|
|
statefulset["spec"]["template"]["metadata"]["annotations"][
|
|
"ai.bstein.dev/config-rev"
|
|
]
|
|
== "20260816-telegram-topics"
|
|
)
|
|
pod_spec = statefulset["spec"]["template"]["spec"]
|
|
patch_init = next(
|
|
item
|
|
for item in pod_spec["initContainers"]
|
|
if item["name"] == "patch-stream-recovery"
|
|
)
|
|
assert patch_init["command"][-2:] == [
|
|
"/opt/hermes/agent/conversation_loop.py",
|
|
"/patched/conversation_loop.py",
|
|
]
|
|
hermes = next(item for item in pod_spec["containers"] if item["name"] == "hermes")
|
|
assert {
|
|
"name": "stream-recovery-patch",
|
|
"mountPath": "/opt/hermes/agent/conversation_loop.py",
|
|
"subPath": "conversation_loop.py",
|
|
} in hermes["volumeMounts"]
|
|
api_session_init = next(
|
|
item
|
|
for item in pod_spec["initContainers"]
|
|
if item["name"] == "patch-api-server-sessions"
|
|
)
|
|
assert "patch_api_server_sessions.py" in api_session_init["args"][0]
|
|
assert "migrate_telegram_api_sessions.py" in api_session_init["args"][0]
|
|
assert {
|
|
"name": "api-server-patch",
|
|
"mountPath": "/opt/hermes/gateway/platforms/api_server.py",
|
|
"subPath": "api_server.py",
|
|
} in hermes["volumeMounts"]
|
|
assert any(volume["name"] == "api-server-patch" for volume in pod_spec["volumes"])
|
|
assert not any(
|
|
mount["mountPath"].endswith("/.codex") for mount in hermes["volumeMounts"]
|
|
)
|
|
|
|
policies = _documents(HERMES / "networkpolicy.yaml")
|
|
agent_policy = next(
|
|
item
|
|
for item in policies
|
|
if item["metadata"]["name"] == "hermes-agent-isolation"
|
|
)
|
|
broker_ingress = next(
|
|
rule
|
|
for rule in agent_policy["spec"]["ingress"]
|
|
if {port["port"] for port in rule["ports"]} == {9002, 9003}
|
|
)
|
|
assert broker_ingress["from"][0]["podSelector"]["matchLabels"] == {
|
|
"app": "hermes-chat-tenant"
|
|
}
|