Release v0.51.316 — Release KF (Phase 2: agent-source dependency audit, #3723) (#3785)
Some checks failed
Release & Docker / release (push) Has been cancelled

Phase-2 release. #3723 (@rodboev): read-only agent-source dependency audit script + contract docs + test, no runtime change. (#3781 Polish locale dropped from batch — incomplete locale, kicked back to contributor.) Full suite 8199 passed, Opus SHIP, Codex clean. Co-authored-by: rodboev <rodboev@users.noreply.github.com>
This commit is contained in:
nesquena-hermes
2026-06-07 10:38:49 -07:00
committed by GitHub
parent ee229f32ad
commit 07235674ad
5 changed files with 782 additions and 0 deletions

View File

@@ -3,6 +3,11 @@
## [Unreleased]
## [v0.51.316] — 2026-06-07 — Release KF (Phase 2 — agent-source dependency audit contract)
### Changed
- **Documented the WebUI → hermes-agent source-dependency contract.** Adds a deterministic, repo-relative audit script (`scripts/audit_agent_source_dependencies.py`) that classifies how the WebUI depends on the agent source tree, an architecture/contract doc, and a regression test pinning the dependency classes. Read-only tooling and docs — no runtime behavior change; groundwork for cleaner agent/WebUI packaging boundaries. (#3723, @rodboev)
## [v0.51.315] — 2026-06-07 — Release KE (test infra — cross-platform workspace-fallback tests)
### Changed

View File

@@ -42,6 +42,10 @@ does not change runtime behavior, maintainer policy, bot behavior, or CI gates.
execution behind an adapter boundary. Use this for adapter-seam, control-plane,
runner, sidecar, or execution-ownership work; do not treat it as authorization
to implement those slices.
- [`docs/architecture/agent-api-contract.md`](architecture/agent-api-contract.md):
current audit of WebUI dependencies on the hermes-agent source checkout and
the replacement API/client surfaces needed before source mounts can be removed.
Start here for issue #2491 and Docker/source-boundary migration slices.
- [`docs/rfcs/turn-journal.md`](rfcs/turn-journal.md): proposed crash-safe
write-ahead journal for browser-originated chat turns.
- [`docs/rfcs/README.md`](rfcs/README.md): RFC conventions and current RFC index.

View File

@@ -0,0 +1,111 @@
# WebUI to hermes-agent source dependency contract
This document records the current WebUI dependency on the hermes-agent source
tree for issue #2491. It is an audit and replacement contract only; it does not
change runtime behavior or Docker wiring.
Run the deterministic audit with:
```powershell
python scripts/audit_agent_source_dependencies.py
python scripts/audit_agent_source_dependencies.py --format markdown
```
The JSON output is repo-relative and grouped by stable dependency class IDs so
follow-up PRs can remove one class at a time without relying on exact line
fixtures.
## Current boundary
The multi-container setup still shares the agent source tree with the WebUI:
- `docker-compose.two-container.yml` mounts `hermes-agent-src` at `/opt/hermes`
in the agent service and read-only at
`/home/hermeswebui/.hermes/hermes-agent` in the WebUI service.
- `docker-compose.three-container.yml` uses the same source volume pattern.
- `docker_init.bash` documents that the WebUI-side mount is read-only and uses
a staged copy when installing from the source checkout.
- `docs/docker.md` explicitly documents that this is not a filesystem trust
boundary: the WebUI cannot write the mount, but it still installs and imports
code from it.
The durable target is that multi-container WebUI should not need a direct
`hermes-agent-src` mount. The WebUI should communicate with hermes-agent through
HTTP endpoints and a small versioned client/shared-schema package where pure
helpers are genuinely shared code.
## Dependency classes
| Audit class | Current surface | Replacement classification |
| --- | --- | --- |
| `docker_agent_source_volume` | Compose files and Docker docs expose `hermes-agent-src` and `/opt/hermes` to make the agent checkout visible to WebUI. | Remove the WebUI source mount only after startup install and runtime imports have migrated. This needs Docker/compose follow-up work, not a runtime behavior change in this audit PR. |
| `startup_dependency_install` | `api/startup.py` discovers `HERMES_WEBUI_AGENT_DIR` or `$HERMES_HOME/hermes-agent`; `server.py` calls `auto_install_agent_deps()` after import verification fails; `docker_init.bash` installs from the staged agent source. | Replace source-tree pip installs with a packaged hermes-agent WebUI client plus an agent health/version capability contract. Keep `HERMES_WEBUI_AGENT_DIR` during migration as an override/debug path, but it should stop being required in normal multi-container startup. |
| `runtime_auxiliary_model_metadata` | `api/streaming.py`, `api/routes.py`, `api/config.py`, and `api/providers.py` import `agent.auxiliary_client`, `agent.model_metadata`, `agent.models_dev`, `hermes_cli.models`, and `agent.account_usage`. | Existing provider/model WebUI endpoints can keep serving UI data where they already wrap agent helpers. Missing surfaces need hermes-agent endpoints or a client package for auxiliary task config, text auxiliary calls, context length, token estimate, provider catalog, and account usage. |
| `runtime_session_state` | `api/streaming.py`, `api/goals.py`, and `api/state_sync.py` import `hermes_state.SessionDB` directly. | Move cross-container state reads and writes behind hermes-agent session/state endpoints. WebUI-only presentation state can remain local, but agent session storage should not be opened from the WebUI container. |
| `runtime_gateway_provider` | `api/streaming.py` and `api/routes.py` import `hermes_cli.runtime_provider`; adapter helpers such as `agent.anthropic_adapter` are also imported for gateway normalization. | Provider resolution, runtime routing, and gateway invocation should be hermes-agent API calls. WebUI can keep request validation and display formatting, but it should not import runtime provider internals from the agent checkout. |
| `webui_local_or_client_package` | WebUI imports `hermes_cli.auth`, `hermes_cli.config`, `hermes_cli.plugins`, `hermes_cli.profiles`, `hermes_cli.goals`, `agent.skill_utils`, `agent.credential_pool`, and `hermes_constants`. | Pure schemas, constants, and parsing helpers can move into a small versioned client/shared package. Privileged data such as credential pools, auth status, profile mutation, plugin discovery, and goal persistence need hermes-agent endpoints. UI-only formatting can remain in WebUI. |
## Replacement contract
### Existing endpoint candidates
The WebUI already exposes provider, model, profile, route, and streaming
handlers that callers use today. Those handlers can remain as WebUI HTTP routes
when they only format UI responses, but their implementations should stop
loading agent modules directly. Good candidates for reusing the current WebUI
route shape while changing its backend dependency are:
- Provider/model catalog routes currently backed by `hermes_cli.models`.
- Auxiliary title/compression paths currently backed by `agent.auxiliary_client`.
- Context-window and token-estimate paths currently backed by
`agent.model_metadata`.
- Runtime-provider choices currently backed by `hermes_cli.runtime_provider`.
### New hermes-agent endpoints needed
These surfaces require an agent-owned endpoint because they read agent state,
perform provider/runtime decisions, or expose privileged data:
- SessionDB/session state read and write operations now using
`hermes_state.SessionDB`.
- Runtime provider resolution and gateway normalization now using
`hermes_cli.runtime_provider` and `agent.anthropic_adapter`.
- Auxiliary task execution and configuration now using `agent.auxiliary_client`.
- Credential/auth/account usage access now using `agent.credential_pool`,
`hermes_cli.auth`, and `agent.account_usage`.
- Profile, plugin, goal, and skill operations that mutate or discover
agent-owned resources.
### Client/shared package candidates
These items can be kept out of the live agent API if they are pure, versioned,
and safe to import without the agent source tree:
- Shared constants currently imported from `hermes_constants`.
- Provider/model schema names and non-privileged catalog shape definitions.
- Pure skill/profile parsing helpers that do not touch agent-owned state.
- Typed response/request models for the new hermes-agent endpoints.
### WebUI-local items
The WebUI can keep code that is only presentation, validation, or routing glue:
- User-facing diagnostics that display whether `HERMES_WEBUI_AGENT_DIR` is set.
- Route-level request validation and response formatting.
- WebUI-only caches and client-facing state that do not open agent SessionDB.
- Docker documentation describing the transition while both paths are supported.
## Audit expectations
`tests/test_agent_source_dependency_audit.py` pins the contract shape:
- Docker/compose source sharing is reported.
- Startup dependency installation and `HERMES_WEBUI_AGENT_DIR` are reported.
- Runtime auxiliary/model metadata imports are reported.
- Runtime SessionDB/state imports are reported.
- Runtime provider/gateway imports are reported.
- The catch-all class for local/client-package candidates remains populated.
The tests intentionally check stable class IDs and representative anchors, not
exact full fixtures. Follow-up migration PRs should update this document and the
audit expectations when a dependency class is intentionally reduced or removed.

View File

@@ -0,0 +1,464 @@
#!/usr/bin/env python3
"""Audit WebUI dependencies on the hermes-agent source tree.
This report is deterministic and repo-relative so migration PRs can compare the
same dependency classes without relying on brittle exact line fixtures.
"""
from __future__ import annotations
import argparse
import ast
import json
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable
AGENT_MODULE_ROOTS = (
"agent",
"cron",
"hermes_cli",
"hermes_constants",
"hermes_state",
"run_agent",
"tools",
)
@dataclass(frozen=True)
class Finding:
path: str
line: int
kind: str
anchor: str
text: str
@dataclass(frozen=True)
class DependencyClass:
class_id: str
title: str
current_dependency: str
replacement_surface: str
findings: tuple[Finding, ...]
def _repo_root_from_script() -> Path:
return Path(__file__).resolve().parents[1]
def _line_text(path: Path, line_number: int) -> str:
try:
return path.read_text(encoding="utf-8").splitlines()[line_number - 1].strip()
except (IndexError, OSError, UnicodeDecodeError):
return ""
def _iter_text_matches(
root: Path,
paths: Iterable[str],
patterns: Iterable[tuple[str, str]],
) -> list[Finding]:
findings: list[Finding] = []
compiled = [(kind, re.compile(pattern)) for kind, pattern in patterns]
for rel in sorted(paths):
path = root / rel
if not path.is_file():
continue
try:
lines = path.read_text(encoding="utf-8").splitlines()
except UnicodeDecodeError:
continue
for line_number, line in enumerate(lines, start=1):
for kind, pattern in compiled:
match = pattern.search(line)
if match:
findings.append(
Finding(
path=rel.replace("\\", "/"),
line=line_number,
kind=kind,
anchor=match.group(0),
text=line.strip(),
)
)
break
return findings
def _iter_python_files(root: Path) -> list[Path]:
paths: set[Path] = set()
api_dir = root / "api"
if api_dir.is_dir():
paths.update(api_dir.rglob("*.py"))
for filename in ("server.py", "bootstrap.py"):
path = root / filename
if path.is_file():
paths.add(path)
return sorted(paths)
def _module_root(module_name: str) -> str:
return module_name.split(".", 1)[0]
def _import_kind(module_name: str) -> str:
if module_name == "run_agent" or module_name.startswith("run_agent."):
return "runtime_agent_execution_import"
if module_name == "tools" or module_name.startswith("tools."):
return "runtime_tools_import"
if module_name == "cron" or module_name.startswith("cron."):
return "runtime_cron_import"
if module_name == "hermes_state" or module_name.startswith("hermes_state."):
return "state_import"
if module_name == "hermes_constants" or module_name.startswith("hermes_constants."):
return "constants_import"
if module_name.startswith("hermes_cli.runtime_provider"):
return "runtime_provider_import"
if module_name.startswith(("agent.auxiliary_client", "agent.model_metadata", "agent.models_dev")):
return "auxiliary_model_metadata_import"
if module_name.startswith(("hermes_cli.models", "agent.account_usage")):
return "provider_model_catalog_import"
if module_name.startswith(("hermes_cli.auth", "hermes_cli.config", "agent.credential_pool")):
return "auth_config_credential_import"
if module_name.startswith(("agent.skill_utils", "hermes_cli.plugins", "hermes_cli.profiles", "hermes_cli.goals")):
return "profiles_skills_plugins_import"
if module_name.startswith("agent.anthropic_adapter"):
return "gateway_adapter_import"
return "agent_source_import"
def _runtime_import_findings(root: Path) -> list[Finding]:
findings: list[Finding] = []
for path in _iter_python_files(root):
rel = path.relative_to(root).as_posix()
try:
source = path.read_text(encoding="utf-8")
tree = ast.parse(source, filename=rel)
except (SyntaxError, UnicodeDecodeError):
continue
for node in ast.walk(tree):
module_name = ""
if isinstance(node, ast.ImportFrom) and node.module:
module_name = node.module
elif isinstance(node, ast.Import):
for alias in node.names:
if _module_root(alias.name) in AGENT_MODULE_ROOTS:
findings.append(
Finding(
path=rel,
line=node.lineno,
kind=_import_kind(alias.name),
anchor=alias.name,
text=_line_text(path, node.lineno),
)
)
continue
if module_name and _module_root(module_name) in AGENT_MODULE_ROOTS:
findings.append(
Finding(
path=rel,
line=node.lineno,
kind=_import_kind(module_name),
anchor=module_name,
text=_line_text(path, node.lineno),
)
)
if isinstance(node, ast.Assign) and isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
findings.extend(_embedded_python_import_findings(rel, node.lineno, node.value.value))
return sorted(findings, key=lambda item: (item.path, item.line, item.anchor))
def _embedded_python_import_findings(rel: str, base_line: int, source: str) -> list[Finding]:
if "import " not in source:
return []
try:
tree = ast.parse(source, filename=f"{rel}:embedded")
except SyntaxError:
return []
lines = source.splitlines()
findings: list[Finding] = []
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
if _module_root(alias.name) in AGENT_MODULE_ROOTS:
line = base_line + node.lineno - 1
findings.append(
Finding(
path=rel,
line=line,
kind=_import_kind(alias.name),
anchor=alias.name,
text=lines[node.lineno - 1].strip() if node.lineno - 1 < len(lines) else "",
)
)
elif isinstance(node, ast.ImportFrom) and node.module and _module_root(node.module) in AGENT_MODULE_ROOTS:
line = base_line + node.lineno - 1
findings.append(
Finding(
path=rel,
line=line,
kind=_import_kind(node.module),
anchor=node.module,
text=lines[node.lineno - 1].strip() if node.lineno - 1 < len(lines) else "",
)
)
return findings
def _findings_by_kind(findings: Iterable[Finding], kinds: set[str]) -> tuple[Finding, ...]:
selected = [finding for finding in findings if finding.kind in kinds]
return tuple(sorted(selected, key=lambda item: (item.path, item.line, item.anchor)))
def build_report(root: Path) -> dict[str, object]:
root = root.resolve()
docker_findings = _iter_text_matches(
root,
(
"docker-compose.two-container.yml",
"docker-compose.three-container.yml",
"docker_init.bash",
"docs/docker.md",
"docs/rfcs/agent-source-boundary.md",
),
(
("agent_source_volume", r"hermes-agent-src"),
("agent_source_path", r"/opt/hermes(?:\b|[-/])?"),
),
)
startup_findings = _iter_text_matches(
root,
(
"server.py",
"bootstrap.py",
"start.ps1",
"api/startup.py",
"api/config.py",
"api/streaming.py",
"docker_init.bash",
),
(
("startup_install_function", r"auto_install_agent_deps"),
("agent_dir_env", r"HERMES_WEBUI_AGENT_DIR"),
("agent_source_install", r"uv pip install.*\[all\]"),
("agent_source_staging", r"_agent_src|_stage_src"),
),
)
runtime_findings = _runtime_import_findings(root)
classes = (
DependencyClass(
class_id="docker_agent_source_volume",
title="Docker/compose source-tree sharing",
current_dependency=(
"Multi-container compose files expose the agent image source via "
"the hermes-agent-src volume and /opt/hermes."
),
replacement_surface=(
"Remove the WebUI source mount after startup install and runtime "
"imports move to hermes-agent endpoints or a versioned client package."
),
findings=_findings_by_kind(docker_findings, {"agent_source_volume", "agent_source_path"}),
),
DependencyClass(
class_id="startup_dependency_install",
title="Startup dependency installation from agent checkout",
current_dependency=(
"WebUI startup discovers HERMES_WEBUI_AGENT_DIR or ~/.hermes/hermes-agent "
"and installs the agent checkout extras."
),
replacement_surface=(
"Replace source-tree pip installs with a packaged hermes-agent client "
"or an agent health/version endpoint that declares required WebUI client capabilities."
),
findings=_findings_by_kind(
startup_findings,
{"startup_install_function", "agent_dir_env", "agent_source_install", "agent_source_staging"},
),
),
DependencyClass(
class_id="runtime_agent_execution",
title="Runtime agent execution, tools, and cron imports",
current_dependency=(
"Browser chat, approvals, tools, and scheduled work import Hermes Agent "
"execution modules directly from the source checkout."
),
replacement_surface=(
"Move run orchestration, tool approval/control, and cron execution behind "
"Hermes Agent APIs or a versioned client package before removing source mounts."
),
findings=_findings_by_kind(
runtime_findings,
{"runtime_agent_execution_import", "runtime_tools_import", "runtime_cron_import"},
),
),
DependencyClass(
class_id="runtime_auxiliary_model_metadata",
title="Runtime auxiliary and model metadata imports",
current_dependency=(
"WebUI imports agent auxiliary client and model metadata helpers directly "
"for title/compression tasks, context windows, provider catalogs, and account usage."
),
replacement_surface=(
"Use existing provider/model endpoints where they already exist; add agent "
"endpoints or a client package for auxiliary task config, text auxiliary "
"calls, context lengths, token estimates, provider catalog, and account usage."
),
findings=_findings_by_kind(
runtime_findings,
{"auxiliary_model_metadata_import", "provider_model_catalog_import"},
),
),
DependencyClass(
class_id="runtime_session_state",
title="Runtime SessionDB/state access",
current_dependency=(
"WebUI imports hermes_state.SessionDB and related state helpers directly "
"to read or write agent session state."
),
replacement_surface=(
"Move cross-container state reads/writes behind hermes-agent session/state "
"endpoints; keep WebUI-only presentation caches local."
),
findings=_findings_by_kind(runtime_findings, {"state_import"}),
),
DependencyClass(
class_id="runtime_gateway_provider",
title="Gateway/runtime provider calls",
current_dependency=(
"WebUI imports hermes_cli.runtime_provider and agent adapter helpers "
"to resolve providers and normalize gateway calls."
),
replacement_surface=(
"Route provider resolution and gateway invocation through hermes-agent "
"runtime/provider endpoints; keep only display formatting in WebUI."
),
findings=_findings_by_kind(
runtime_findings,
{"runtime_provider_import", "gateway_adapter_import"},
),
),
DependencyClass(
class_id="webui_local_or_client_package",
title="Config, auth, skills, profiles, plugins, and constants imports",
current_dependency=(
"WebUI imports hermes_cli and agent helpers for config/auth status, "
"credential pools, skills, profiles, plugin discovery, goals, and constants."
),
replacement_surface=(
"Keep WebUI-owned display/config code local, but move shared schemas and "
"pure helpers into a versioned client package; privileged agent data needs endpoints."
),
findings=_findings_by_kind(
runtime_findings,
{
"auth_config_credential_import",
"profiles_skills_plugins_import",
"constants_import",
"agent_source_import",
},
),
),
)
class_dicts: list[dict[str, object]] = []
for item in classes:
findings = [
{
"path": finding.path,
"line": finding.line,
"kind": finding.kind,
"anchor": finding.anchor,
"text": finding.text,
}
for finding in item.findings
]
class_dicts.append(
{
"id": item.class_id,
"title": item.title,
"current_dependency": item.current_dependency,
"replacement_surface": item.replacement_surface,
"finding_count": len(findings),
"findings": findings,
}
)
return {
"schema_version": 1,
"repo_root": ".",
"summary": {
"dependency_class_count": len(class_dicts),
"finding_count": sum(int(item["finding_count"]) for item in class_dicts),
"dependency_class_ids": [item["id"] for item in class_dicts],
},
"dependency_classes": class_dicts,
}
def _print_markdown(report: dict[str, object]) -> None:
print("# Agent Source Dependency Audit")
print()
summary = report["summary"]
assert isinstance(summary, dict)
print(f"- Schema version: {report['schema_version']}")
print(f"- Dependency classes: {summary['dependency_class_count']}")
print(f"- Findings: {summary['finding_count']}")
print()
classes = report["dependency_classes"]
assert isinstance(classes, list)
for item in classes:
assert isinstance(item, dict)
print(f"## {item['id']}: {item['title']}")
print()
print(f"- Current dependency: {item['current_dependency']}")
print(f"- Replacement surface: {item['replacement_surface']}")
print(f"- Findings: {item['finding_count']}")
print()
findings = item["findings"]
assert isinstance(findings, list)
for finding in findings:
print(
f"- `{finding['path']}:{finding['line']}` "
f"({finding['kind']}, `{finding['anchor']}`): {finding['text']}"
)
print()
def main(argv: list[str] | None = None) -> int:
for stream in (sys.stdout, sys.stderr):
try:
stream.reconfigure(encoding="utf-8")
except Exception:
pass
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"repo_root",
nargs="?",
type=Path,
default=_repo_root_from_script(),
help="Path to the hermes-webui checkout; defaults to this script's repo.",
)
parser.add_argument(
"--format",
choices=("json", "markdown"),
default="json",
help="Output format. JSON is stable for tests and automation.",
)
args = parser.parse_args(argv)
report = build_report(args.repo_root)
if args.format == "markdown":
_print_markdown(report)
else:
print(json.dumps(report, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))

View File

@@ -0,0 +1,198 @@
from __future__ import annotations
import json
import importlib.util
import subprocess
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
AUDIT_SCRIPT = REPO / "scripts" / "audit_agent_source_dependencies.py"
def _audit_module():
spec = importlib.util.spec_from_file_location("audit_agent_source_dependencies", AUDIT_SCRIPT)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def _run_audit() -> dict[str, object]:
proc = subprocess.run(
[sys.executable, str(AUDIT_SCRIPT), str(REPO)],
cwd=REPO,
capture_output=True,
text=True,
check=True,
)
return json.loads(proc.stdout)
def _run_markdown_audit() -> str:
proc = subprocess.run(
[sys.executable, str(AUDIT_SCRIPT), str(REPO), "--format", "markdown"],
cwd=REPO,
capture_output=True,
text=True,
check=True,
)
return proc.stdout
def _class_by_id(report: dict[str, object]) -> dict[str, dict[str, object]]:
classes = report["dependency_classes"]
assert isinstance(classes, list)
return {item["id"]: item for item in classes}
def _anchors(dep_class: dict[str, object]) -> set[tuple[str, str]]:
findings = dep_class["findings"]
assert isinstance(findings, list)
return {
(str(finding["path"]), str(finding["anchor"]))
for finding in findings
}
def _texts(dep_class: dict[str, object]) -> list[str]:
findings = dep_class["findings"]
assert isinstance(findings, list)
return [str(finding["text"]) for finding in findings]
def _findings(dep_class: dict[str, object]) -> list[dict[str, object]]:
findings = dep_class["findings"]
assert isinstance(findings, list)
return findings
def test_audit_reports_expected_dependency_classes():
report = _run_audit()
assert report["schema_version"] == 1
classes = _class_by_id(report)
assert set(classes) == {
"docker_agent_source_volume",
"startup_dependency_install",
"runtime_agent_execution",
"runtime_auxiliary_model_metadata",
"runtime_session_state",
"runtime_gateway_provider",
"webui_local_or_client_package",
}
for dep_class in classes.values():
assert dep_class["finding_count"] > 0
assert dep_class["findings"]
assert dep_class["replacement_surface"]
def test_audit_reports_compose_source_volume_anchors():
classes = _class_by_id(_run_audit())
anchors = _anchors(classes["docker_agent_source_volume"])
texts = _texts(classes["docker_agent_source_volume"])
assert ("docker-compose.two-container.yml", "hermes-agent-src") in anchors
assert ("docker-compose.three-container.yml", "hermes-agent-src") in anchors
assert any("hermes-agent-src:/opt/hermes" in text for text in texts)
assert any(
"hermes-agent-src:/home/hermeswebui/.hermes/hermes-agent:ro" in text
for text in texts
)
def test_audit_reports_startup_install_dependencies():
classes = _class_by_id(_run_audit())
anchors = _anchors(classes["startup_dependency_install"])
texts = _texts(classes["startup_dependency_install"])
assert ("api/startup.py", "HERMES_WEBUI_AGENT_DIR") in anchors
assert ("bootstrap.py", "HERMES_WEBUI_AGENT_DIR") in anchors
assert ("start.ps1", "HERMES_WEBUI_AGENT_DIR") in anchors
assert ("api/startup.py", "auto_install_agent_deps") in anchors
assert ("server.py", "auto_install_agent_deps") in anchors
assert any("uv pip install" in text and "[all]" in text for text in texts)
def test_audit_reports_runtime_agent_execution_imports():
classes = _class_by_id(_run_audit())
anchors = _anchors(classes["runtime_agent_execution"])
assert ("api/streaming.py", "run_agent") in anchors
assert ("api/routes.py", "tools.skills_tool") in anchors
assert ("api/streaming.py", "tools.approval") in anchors
assert ("api/routes.py", "cron.jobs") in anchors
def test_audit_reports_runtime_auxiliary_and_model_metadata_imports():
classes = _class_by_id(_run_audit())
anchors = _anchors(classes["runtime_auxiliary_model_metadata"])
assert ("api/streaming.py", "agent.auxiliary_client") in anchors
assert ("api/streaming.py", "agent.model_metadata") in anchors
assert ("api/config.py", "hermes_cli.models") in anchors
assert ("api/providers.py", "agent.account_usage") in anchors
def test_audit_embedded_worker_import_line_anchors_are_source_lines():
classes = _class_by_id(_run_audit())
provider_findings = _findings(classes["runtime_auxiliary_model_metadata"])
account_usage = next(
finding
for finding in provider_findings
if finding["path"] == "api/providers.py" and finding["anchor"] == "agent.account_usage"
)
assert account_usage["line"] == 168
assert account_usage["text"] == "from agent.account_usage import fetch_account_usage"
def test_audit_reports_runtime_state_and_provider_imports():
classes = _class_by_id(_run_audit())
state_anchors = _anchors(classes["runtime_session_state"])
provider_anchors = _anchors(classes["runtime_gateway_provider"])
assert ("api/streaming.py", "hermes_state") in state_anchors
assert ("api/state_sync.py", "hermes_state") in state_anchors
assert ("api/streaming.py", "hermes_cli.runtime_provider") in provider_anchors
assert ("api/routes.py", "hermes_cli.runtime_provider") in provider_anchors
def test_runtime_import_scan_includes_root_python_entrypoints():
root = AUDIT_SCRIPT.parents[1]
audit_module = _audit_module()
paths = {
path.relative_to(root).as_posix()
for path in audit_module._iter_python_files(root)
}
assert "api/routes.py" in paths
assert "server.py" in paths
assert "bootstrap.py" in paths
def test_audit_keeps_client_package_candidates_visible():
classes = _class_by_id(_run_audit())
anchors = _anchors(classes["webui_local_or_client_package"])
assert ("api/streaming.py", "hermes_constants") in anchors
assert ("api/routes.py", "agent.skill_utils") in anchors
assert ("api/routes.py", "hermes_cli.plugins") in anchors
assert ("api/providers.py", "agent.credential_pool") in anchors
def test_markdown_output_is_utf8_safe_on_windows_stdout():
markdown = _run_markdown_audit()
assert "# Agent Source Dependency Audit" in markdown
assert "runtime_agent_execution" in markdown
def test_contract_index_links_agent_api_contract():
contracts = (REPO / "docs" / "CONTRACTS.md").read_text(encoding="utf-8")
assert "docs/architecture/agent-api-contract.md" in contracts
assert "issue #2491" in contracts
assert "source mounts can be removed" in contracts