Some checks are pending
CI / lint-and-test (push) Waiting to run
* Harden leoclean no-send staging runtime * Isolate GCP leoclean runtime adapters * Verify current-main leoclean context contracts * Constrain no-send operational contracts
794 lines
31 KiB
Python
794 lines
31 KiB
Python
#!/usr/bin/env python3
|
|
"""Start or inspect the structurally no-send Hermes/leoclean staging runtime."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
import hashlib
|
|
import importlib
|
|
import json
|
|
import os
|
|
import platform
|
|
import shlex
|
|
import signal
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
class NoSendContractError(RuntimeError):
|
|
"""The runtime does not satisfy its no-send contract."""
|
|
|
|
|
|
def _require(condition: bool, message: str) -> None:
|
|
if not condition:
|
|
raise NoSendContractError(message)
|
|
|
|
|
|
class _SealedToolMap(dict[str, Any]):
|
|
"""Read-only registry map after the reviewed tools are installed."""
|
|
|
|
@staticmethod
|
|
def _deny(*_args: Any, **_kwargs: Any) -> None:
|
|
raise NoSendContractError("Hermes tool registry is sealed by the no-send runtime")
|
|
|
|
__setitem__ = _deny
|
|
__delitem__ = _deny
|
|
clear = _deny
|
|
pop = _deny
|
|
popitem = _deny
|
|
setdefault = _deny
|
|
update = _deny
|
|
|
|
|
|
def _load_json(path: Path) -> dict[str, Any]:
|
|
try:
|
|
value = json.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError) as exc:
|
|
raise NoSendContractError(f"cannot load {path.name}: {type(exc).__name__}") from exc
|
|
_require(isinstance(value, dict), f"{path.name} must be a JSON object")
|
|
return value
|
|
|
|
|
|
def _sha256(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as handle:
|
|
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def _runtime_root() -> Path:
|
|
return Path(__file__).resolve().parent
|
|
|
|
|
|
def _validate_contract(value: dict[str, Any]) -> None:
|
|
_require(
|
|
set(value)
|
|
== {
|
|
"schema",
|
|
"runtime_mode",
|
|
"transport_authority",
|
|
"service_mode",
|
|
"handler_platform",
|
|
"plugins",
|
|
"toolset",
|
|
"profile",
|
|
"environment",
|
|
"database",
|
|
"claim_ceiling",
|
|
},
|
|
"no-send runtime contract fields are not exact",
|
|
)
|
|
_require(
|
|
value.get("schema") == "livingip.leocleanNoSendRuntimeContract.v1",
|
|
"no-send runtime contract schema is invalid",
|
|
)
|
|
_require(value.get("runtime_mode") == "gcp_staging_no_send", "runtime mode is not no-send staging")
|
|
_require(value.get("transport_authority") == "absent", "transport authority is not absent")
|
|
_require(value.get("service_mode") == "gateway_without_platform_adapters", "service mode is not no-send")
|
|
_require(value.get("handler_platform") == "api_server", "handler platform is not the reviewed surface")
|
|
_require(
|
|
value.get("plugins")
|
|
== {
|
|
"allowed": [
|
|
{
|
|
"name": "leo-db-context",
|
|
"source": "user",
|
|
"tools": [],
|
|
"hooks": ["post_llm_call", "pre_llm_call"],
|
|
}
|
|
]
|
|
},
|
|
"plugin contract is not exact",
|
|
)
|
|
toolset = value.get("toolset") or {}
|
|
_require(
|
|
toolset.get("allowed_tools") == ["skills_list", "skill_view", "terminal"],
|
|
"allowed tool contract is not exact",
|
|
)
|
|
_require(
|
|
set(toolset.get("forbidden_tools") or [])
|
|
== {"cronjob", "delegate_task", "memory", "process", "send_message", "skill_manage"},
|
|
"forbidden tool contract is not exact",
|
|
)
|
|
_require(
|
|
set(toolset.get("terminal_commands") or [])
|
|
== {
|
|
"context",
|
|
"decision-matrix-status",
|
|
"edges",
|
|
"evidence",
|
|
"list-proposals",
|
|
"propose-core-change",
|
|
"propose-edge",
|
|
"search",
|
|
"search-proposals",
|
|
"show",
|
|
"show-proposal",
|
|
"status",
|
|
},
|
|
"terminal command contract is not exact",
|
|
)
|
|
profile = value.get("profile") or {}
|
|
_require(
|
|
set(profile.get("template_entries") or []) == {"bin", "config.yaml", "plugins", "skills"},
|
|
"profile template contract is not exact",
|
|
)
|
|
_require(
|
|
set(profile.get("required_runtime_entries") or [])
|
|
== {"SOUL.md", "identity-lock.json", "identity-manifest.json"},
|
|
"runtime identity contract is not exact",
|
|
)
|
|
_require(
|
|
set(profile.get("forbidden_entries") or [])
|
|
== {".env", "BOOT.md", "auth.json", "gateway.json", "hooks", "processes.json", "secrets"},
|
|
"forbidden profile contract is not exact",
|
|
)
|
|
learning_policy = profile.get("learning_policy") or {}
|
|
_require(
|
|
set(learning_policy)
|
|
== {
|
|
"automatic_memory_review_interval",
|
|
"automatic_skill_review_interval",
|
|
"memory_enabled",
|
|
"user_profile_enabled",
|
|
}
|
|
and learning_policy.get("memory_enabled") is False
|
|
and learning_policy.get("user_profile_enabled") is False
|
|
and type(learning_policy.get("automatic_memory_review_interval")) is int
|
|
and learning_policy.get("automatic_memory_review_interval") == 0
|
|
and type(learning_policy.get("automatic_skill_review_interval")) is int
|
|
and learning_policy.get("automatic_skill_review_interval") == 0,
|
|
"learning policy contract is not exact",
|
|
)
|
|
routing_policy = profile.get("routing_policy") or {}
|
|
_require(
|
|
set(routing_policy) == {"cheap_model", "smart_model_routing"}
|
|
and routing_policy.get("cheap_model") == "absent"
|
|
and routing_policy.get("smart_model_routing") is False,
|
|
"routing policy contract is not exact",
|
|
)
|
|
environment = value.get("environment") or {}
|
|
_require(
|
|
environment.get("allowed_provider_credentials") == ["OPENROUTER_API_KEY"],
|
|
"provider credential contract is not exact",
|
|
)
|
|
_require(
|
|
set(environment.get("allowed_runtime_variables") or [])
|
|
== {"HERMES_HOME", "PYTHONDONTWRITEBYTECODE", "PYTHONNOUSERSITE", "PYTHONSAFEPATH"},
|
|
"runtime environment allowlist is not exact",
|
|
)
|
|
required_forbidden_exact = {
|
|
"ALL_PROXY",
|
|
"BASH_ENV",
|
|
"BOT_TOKEN",
|
|
"ENV",
|
|
"GATEWAY_ALLOW_ALL_USERS",
|
|
"GATEWAY_ALLOWED_USERS",
|
|
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
"HERMES_ENABLE_PROJECT_PLUGINS",
|
|
"HERMES_SESSION_PLATFORM",
|
|
"HTTP_PROXY",
|
|
"HTTPS_PROXY",
|
|
"LD_PRELOAD",
|
|
"PYTHONHOME",
|
|
"PYTHONPATH",
|
|
"TELEGRAM_BOT_TOKEN",
|
|
}
|
|
_require(
|
|
set(environment.get("forbidden_exact") or []) == required_forbidden_exact,
|
|
"forbidden environment contract is not exact",
|
|
)
|
|
_require(
|
|
set(environment.get("forbidden_prefixes") or [])
|
|
== {
|
|
"CLOUDSDK_",
|
|
"DINGTALK_",
|
|
"DISCORD_",
|
|
"EMAIL_",
|
|
"FEISHU_",
|
|
"HERMES_",
|
|
"LD_",
|
|
"MATRIX_",
|
|
"MATTERMOST_",
|
|
"SIGNAL_",
|
|
"SLACK_",
|
|
"SMS_",
|
|
"TELEGRAM_",
|
|
"TWILIO_",
|
|
"WEBCOM_",
|
|
"WEBHOOK_",
|
|
"WHATSAPP_",
|
|
},
|
|
"transport environment prefix contract is not exact",
|
|
)
|
|
_require(
|
|
set(environment.get("forbidden_suffixes") or []) == {"_PASSWORD", "_SECRET", "_TOKEN", "_API_KEY"},
|
|
"secret-shaped environment contract is not exact",
|
|
)
|
|
database = value.get("database") or {}
|
|
_require(
|
|
database
|
|
== {
|
|
"automatic_context_modes": ["claim_evidence_challenge", "review_only_candidate"],
|
|
"canonical_database": "teleo_canonical",
|
|
"runtime_role": "leoclean_kb_runtime",
|
|
"proposal_function": "kb_stage.stage_leoclean_proposal(text,text,text,text,jsonb)",
|
|
"direct_canonical_writes": "denied",
|
|
"direct_stage_table_writes": "denied",
|
|
"proposal_staging": "function_only",
|
|
"unsupported_context_policy": "fail_closed",
|
|
},
|
|
"database authority contract is not exact",
|
|
)
|
|
_require(isinstance(value.get("claim_ceiling"), str) and bool(value["claim_ceiling"]), "claim ceiling is missing")
|
|
|
|
|
|
def _contract() -> dict[str, Any]:
|
|
value = _load_json(_runtime_root() / "runtime-contract.json")
|
|
_require(
|
|
value.get("schema") == "livingip.leocleanNoSendRuntimeContract.v1",
|
|
"unsupported no-send runtime contract",
|
|
)
|
|
_validate_contract(value)
|
|
return value
|
|
|
|
|
|
def _validate_environment(contract: dict[str, Any]) -> None:
|
|
policy = contract["environment"]
|
|
allowed_provider = set(policy["allowed_provider_credentials"])
|
|
allowed_runtime = set(policy["allowed_runtime_variables"])
|
|
forbidden_exact = set(policy["forbidden_exact"])
|
|
forbidden_prefixes = tuple(policy["forbidden_prefixes"])
|
|
forbidden_suffixes = tuple(policy["forbidden_suffixes"])
|
|
violations: list[str] = []
|
|
for name, value in os.environ.items():
|
|
if not value:
|
|
continue
|
|
if name in allowed_provider or name in allowed_runtime:
|
|
continue
|
|
if name in forbidden_exact or name.startswith(forbidden_prefixes) or name.endswith(forbidden_suffixes):
|
|
violations.append(name)
|
|
_require(not violations, f"forbidden transport environment is present: {sorted(violations)}")
|
|
|
|
|
|
def _validate_profile(profile: Path, contract: dict[str, Any], *, template: bool) -> None:
|
|
_require(profile.is_dir() and not profile.is_symlink(), "profile root is missing or unsafe")
|
|
forbidden = set(contract["profile"]["forbidden_entries"])
|
|
actual = {path.name for path in profile.iterdir()}
|
|
present_forbidden = sorted(actual & forbidden)
|
|
_require(not present_forbidden, f"forbidden profile entries are present: {present_forbidden}")
|
|
_require(not any(path.is_symlink() for path in profile.rglob("*")), "profile contains a symlink")
|
|
immutable_entries = set(contract["profile"]["template_entries"])
|
|
expected_template = _runtime_root() / "profile-template"
|
|
_require(expected_template.is_dir(), "bound profile template is missing")
|
|
for entry in sorted(immutable_entries):
|
|
expected = expected_template / entry
|
|
observed = profile / entry
|
|
_require(expected.exists() and observed.exists(), f"immutable profile entry is missing: {entry}")
|
|
_require(expected.is_dir() == observed.is_dir(), f"immutable profile entry type drifted: {entry}")
|
|
if expected.is_file():
|
|
_require(_sha256(expected) == _sha256(observed), f"immutable profile file drifted: {entry}")
|
|
_require(
|
|
stat.S_IMODE(expected.stat().st_mode) == stat.S_IMODE(observed.stat().st_mode),
|
|
f"immutable profile file mode drifted: {entry}",
|
|
)
|
|
continue
|
|
expected_files = {
|
|
path.relative_to(expected).as_posix(): (
|
|
_sha256(path),
|
|
stat.S_IMODE(path.stat().st_mode),
|
|
)
|
|
for path in expected.rglob("*")
|
|
if path.is_file()
|
|
}
|
|
observed_files = {
|
|
path.relative_to(observed).as_posix(): (
|
|
_sha256(path),
|
|
stat.S_IMODE(path.stat().st_mode),
|
|
)
|
|
for path in observed.rglob("*")
|
|
if path.is_file()
|
|
}
|
|
_require(observed_files == expected_files, f"immutable profile tree drifted: {entry}")
|
|
if template:
|
|
_require(actual == immutable_entries, "profile template entry set is not exact")
|
|
else:
|
|
required_identity = set(contract["profile"]["required_runtime_entries"])
|
|
state_policy = contract["profile"]["allowed_runtime_state"]
|
|
state_directories = set(state_policy["directories"])
|
|
state_files = set(state_policy["files"])
|
|
allowed = immutable_entries | required_identity | state_directories | state_files
|
|
_require(required_identity <= actual, "compiled runtime profile is incomplete")
|
|
_require(actual <= allowed, f"compiled runtime profile has unapproved entries: {sorted(actual - allowed)}")
|
|
for name in sorted(required_identity):
|
|
entry = profile / name
|
|
_require(
|
|
entry.is_file() and entry.stat().st_size <= 1024 * 1024, f"runtime identity entry is invalid: {name}"
|
|
)
|
|
for name in sorted(actual & state_directories):
|
|
_require((profile / name).is_dir(), f"runtime state entry must be a directory: {name}")
|
|
for name in sorted(actual & state_files):
|
|
_require((profile / name).is_file(), f"runtime state entry must be a file: {name}")
|
|
|
|
|
|
def _load_profile_config(profile: Path, contract: dict[str, Any]) -> dict[str, Any]:
|
|
try:
|
|
import yaml
|
|
|
|
value = yaml.safe_load((profile / "config.yaml").read_text(encoding="utf-8")) or {}
|
|
except (OSError, TypeError, ValueError) as exc:
|
|
raise NoSendContractError(f"cannot load config.yaml: {type(exc).__name__}") from exc
|
|
_require(isinstance(value, dict), "config.yaml must be a mapping")
|
|
expected_top_level = {
|
|
"agent",
|
|
"display",
|
|
"known_plugin_toolsets",
|
|
"mcp_servers",
|
|
"memory",
|
|
"model",
|
|
"platform_toolsets",
|
|
"skills",
|
|
"smart_model_routing",
|
|
}
|
|
_require(set(value) == expected_top_level, "config.yaml top-level fields are not exact")
|
|
_require(value.get("mcp_servers") == {}, "MCP servers are forbidden in no-send staging")
|
|
memory = value.get("memory") or {}
|
|
_require(
|
|
set(memory) == {"enabled", "memory_enabled", "nudge_interval", "user_profile_enabled"}
|
|
and memory.get("enabled") is False
|
|
and memory.get("memory_enabled") is False
|
|
and memory.get("user_profile_enabled") is False
|
|
and type(memory.get("nudge_interval")) is int
|
|
and memory.get("nudge_interval") == 0,
|
|
"persistent memory and automatic memory review must be disabled",
|
|
)
|
|
skills = value.get("skills") or {}
|
|
_require(
|
|
set(skills) == {"creation_nudge_interval"}
|
|
and type(skills.get("creation_nudge_interval")) is int
|
|
and skills.get("creation_nudge_interval") == 0,
|
|
"automatic skill review must be disabled",
|
|
)
|
|
_require(value.get("smart_model_routing") is False, "smart-model routing must be disabled")
|
|
expected_toolset = contract["toolset"]["name"]
|
|
_require(
|
|
value.get("platform_toolsets") == {"api_server": [expected_toolset]},
|
|
"handler toolset binding is not exact",
|
|
)
|
|
_require(
|
|
value.get("known_plugin_toolsets") == {"api_server": []},
|
|
"plugin toolset allowlist is not exact",
|
|
)
|
|
encoded = json.dumps(value, sort_keys=True).lower()
|
|
for marker in (
|
|
"telegram",
|
|
"discord",
|
|
"whatsapp",
|
|
"slack",
|
|
"signal",
|
|
"mattermost",
|
|
"matrix",
|
|
"dingtalk",
|
|
"feishu",
|
|
"wecom",
|
|
"webhook",
|
|
"bot_token",
|
|
"send_message",
|
|
):
|
|
_require(marker not in encoded, f"transport marker is forbidden in config.yaml: {marker}")
|
|
return value
|
|
|
|
|
|
def _load_effective_policy(contract: dict[str, Any]) -> dict[str, Any]:
|
|
"""Prove what the pinned Hermes loader will give the actual agent runtime."""
|
|
|
|
try:
|
|
from agent.smart_model_routing import choose_cheap_model_route
|
|
from hermes_cli.config import load_config
|
|
|
|
effective = load_config()
|
|
except Exception as exc:
|
|
raise NoSendContractError(f"cannot load effective Hermes config: {type(exc).__name__}") from exc
|
|
_require(isinstance(effective, dict), "effective Hermes config must be a mapping")
|
|
learning_policy = contract["profile"]["learning_policy"]
|
|
memory = effective.get("memory") or {}
|
|
skills = effective.get("skills") or {}
|
|
_require(isinstance(memory, dict) and isinstance(skills, dict), "effective learning policy is invalid")
|
|
_require(memory.get("memory_enabled") is False, "effective Hermes memory policy is not disabled")
|
|
_require(memory.get("user_profile_enabled") is False, "effective Hermes user profile is not disabled")
|
|
memory_interval = memory.get("nudge_interval")
|
|
_require(
|
|
type(memory_interval) is int and memory_interval == learning_policy["automatic_memory_review_interval"],
|
|
"effective Hermes memory-review interval is not disabled",
|
|
)
|
|
skill_interval = skills.get("creation_nudge_interval")
|
|
_require(
|
|
type(skill_interval) is int and skill_interval == learning_policy["automatic_skill_review_interval"],
|
|
"effective Hermes skill-review policy is not disabled",
|
|
)
|
|
_require(effective.get("smart_model_routing") is False, "effective Hermes smart routing is not disabled")
|
|
_require(
|
|
choose_cheap_model_route("what time is it?", effective.get("smart_model_routing")) is None,
|
|
"effective Hermes config selected a cheap-model route",
|
|
)
|
|
return {
|
|
"memory_enabled": memory.get("memory_enabled"),
|
|
"user_profile_enabled": memory.get("user_profile_enabled"),
|
|
"memory_nudge_interval": memory.get("nudge_interval"),
|
|
"skill_creation_nudge_interval": skills.get("creation_nudge_interval"),
|
|
"smart_model_routing": False,
|
|
"cheap_model_route": None,
|
|
}
|
|
|
|
|
|
def _restricted_terminal_handler(
|
|
profile: Path,
|
|
allowed_commands: set[str],
|
|
tool_args: dict[str, Any],
|
|
**_kwargs: Any,
|
|
) -> str:
|
|
if not isinstance(tool_args, dict):
|
|
return json.dumps({"output": "", "exit_code": 126, "error": "terminal arguments must be an object"})
|
|
unexpected = sorted(set(tool_args) - {"command", "timeout"})
|
|
if unexpected:
|
|
return json.dumps({"output": "", "exit_code": 126, "error": "terminal arguments contain unsupported fields"})
|
|
command = tool_args.get("command")
|
|
if not isinstance(command, str) or not command.strip():
|
|
return json.dumps({"output": "", "exit_code": 126, "error": "terminal command is required"})
|
|
if "\x00" in command or "\n" in command or "\r" in command:
|
|
return json.dumps(
|
|
{"output": "", "exit_code": 126, "error": "terminal command contains a forbidden control character"}
|
|
)
|
|
try:
|
|
argv = shlex.split(command, posix=True)
|
|
except ValueError as exc:
|
|
return json.dumps({"output": "", "exit_code": 126, "error": f"invalid terminal command: {exc}"})
|
|
wrapper = (profile / "bin" / "teleo-kb").resolve()
|
|
if not argv or argv[0] not in {"teleo-kb", str(wrapper)}:
|
|
return json.dumps({"output": "", "exit_code": 126, "error": "only the reviewed teleo-kb wrapper is allowed"})
|
|
if len(argv) < 2 or argv[1] not in allowed_commands:
|
|
return json.dumps({"output": "", "exit_code": 126, "error": "teleo-kb command is outside the allowlist"})
|
|
forbidden_tokens = {";", "&&", "||", "|", ">", ">>", "<", "<<", "&"}
|
|
forbidden_overrides = {
|
|
"--canonical-db",
|
|
"--credential-mode",
|
|
"--db",
|
|
"--host",
|
|
"--password-secret",
|
|
"--port",
|
|
"--project",
|
|
"--user",
|
|
}
|
|
if any(token in forbidden_tokens for token in argv[2:]):
|
|
return json.dumps({"output": "", "exit_code": 126, "error": "terminal command contains a shell control token"})
|
|
if any(
|
|
token in forbidden_overrides or any(token.startswith(f"{option}=") for option in forbidden_overrides)
|
|
for token in argv[2:]
|
|
):
|
|
return json.dumps({"output": "", "exit_code": 126, "error": "terminal command contains an identity override"})
|
|
try:
|
|
timeout = int(tool_args.get("timeout") or 60)
|
|
except (TypeError, ValueError):
|
|
return json.dumps({"output": "", "exit_code": 126, "error": "terminal timeout must be an integer"})
|
|
timeout = min(max(timeout, 1), 120)
|
|
child_environment = {
|
|
"HOME": str(profile / "state"),
|
|
"HERMES_HOME": str(profile),
|
|
"LANG": "C",
|
|
"LC_ALL": "C",
|
|
"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
}
|
|
try:
|
|
result = subprocess.run(
|
|
[str(wrapper), *argv[1:]],
|
|
cwd=profile,
|
|
env=child_environment,
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
timeout=timeout,
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
return json.dumps({"output": "", "exit_code": 124, "error": "teleo-kb command timed out"})
|
|
return json.dumps(
|
|
{
|
|
"output": result.stdout[:65536],
|
|
"exit_code": result.returncode,
|
|
"error": (result.stderr[:65536] or None),
|
|
"truncated": len(result.stdout) > 65536 or len(result.stderr) > 65536,
|
|
}
|
|
)
|
|
|
|
|
|
def _restricted_terminal_schema(commands: list[str]) -> dict[str, Any]:
|
|
return {
|
|
"name": "terminal",
|
|
"description": "Run one reviewed teleo-kb knowledge command without a shell.",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"command": {
|
|
"type": "string",
|
|
"description": f"teleo-kb followed by one of: {', '.join(commands)}",
|
|
},
|
|
"timeout": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 120,
|
|
},
|
|
},
|
|
"required": ["command"],
|
|
"additionalProperties": False,
|
|
},
|
|
}
|
|
|
|
|
|
def _plugin_inventory(contract: dict[str, Any]) -> list[dict[str, Any]]:
|
|
from hermes_cli.plugins import get_plugin_manager
|
|
|
|
manager = get_plugin_manager()
|
|
observed: list[dict[str, Any]] = []
|
|
for name, loaded in sorted(manager._plugins.items()):
|
|
observed.append(
|
|
{
|
|
"name": name,
|
|
"source": loaded.manifest.source,
|
|
"tools": sorted(loaded.tools_registered),
|
|
"hooks": sorted(loaded.hooks_registered),
|
|
"enabled": loaded.enabled,
|
|
"error": loaded.error,
|
|
}
|
|
)
|
|
expected = [
|
|
{
|
|
**item,
|
|
"tools": sorted(item["tools"]),
|
|
"hooks": sorted(item["hooks"]),
|
|
"enabled": True,
|
|
"error": None,
|
|
}
|
|
for item in contract["plugins"]["allowed"]
|
|
]
|
|
_require(observed == expected, "effective Hermes plugin inventory is not exact")
|
|
return observed
|
|
|
|
|
|
def _assert_tool_registry(profile: Path, contract: dict[str, Any]) -> dict[str, Any]:
|
|
from tools.registry import registry
|
|
|
|
allowed_tools = sorted(contract["toolset"]["allowed_tools"])
|
|
_require(sorted(registry._tools) == allowed_tools, "effective Hermes tool registry is not exact")
|
|
_require(isinstance(registry._tools, _SealedToolMap), "Hermes tool registry is not sealed")
|
|
terminal_entry = registry._tools.get("terminal")
|
|
_require(terminal_entry is not None, "restricted terminal is not registered")
|
|
expected_marker = f"livingip-nosend-terminal:{profile.resolve()}"
|
|
_require(
|
|
getattr(terminal_entry.handler, "_livingip_no_send_handler", None) == expected_marker,
|
|
"restricted terminal handler identity drifted",
|
|
)
|
|
_require(
|
|
terminal_entry.schema == _restricted_terminal_schema(contract["toolset"]["terminal_commands"]),
|
|
"restricted terminal schema drifted",
|
|
)
|
|
for forbidden in contract["toolset"]["forbidden_tools"]:
|
|
_require(forbidden not in registry._tools, f"forbidden Hermes tool remains registered: {forbidden}")
|
|
dispatched = json.loads(registry.dispatch(forbidden, {}))
|
|
_require(
|
|
dispatched == {"error": f"Unknown tool: {forbidden}"}, f"forbidden tool dispatch is not denied: {forbidden}"
|
|
)
|
|
return {
|
|
"toolset": contract["toolset"]["name"],
|
|
"tools": sorted(registry._tools),
|
|
"send_message_present": "send_message" in registry._tools,
|
|
"registry_sealed": True,
|
|
"terminal_handler": "livingip.restricted_teleo_kb.v1",
|
|
"terminal_restricted_to": "profile/bin/teleo-kb",
|
|
"plugins": _plugin_inventory(contract),
|
|
}
|
|
|
|
|
|
def _prepare_tool_surface(profile: Path, config: dict[str, Any], contract: dict[str, Any]) -> dict[str, Any]:
|
|
import toolsets
|
|
from hermes_cli import tools_config
|
|
from tools.registry import registry
|
|
|
|
importlib.import_module("tools.skills_tool")
|
|
importlib.import_module("tools.terminal_tool")
|
|
importlib.import_module("tools.process_registry")
|
|
|
|
toolset_name = contract["toolset"]["name"]
|
|
allowed_tools = list(contract["toolset"]["allowed_tools"])
|
|
toolsets.TOOLSETS[toolset_name] = {
|
|
"description": "LivingIP no-send Cloud SQL knowledge tools",
|
|
"tools": allowed_tools,
|
|
"includes": [],
|
|
}
|
|
resolved = tools_config._get_platform_tools(config, contract["handler_platform"])
|
|
_require(resolved == {toolset_name}, "effective Hermes toolset is not exact")
|
|
missing = sorted(name for name in allowed_tools if name not in registry._tools)
|
|
_require(not missing, f"required Hermes tools are not registered: {missing}")
|
|
_plugin_inventory(contract)
|
|
retained = {name: registry._tools[name] for name in allowed_tools}
|
|
terminal_entry = retained["terminal"]
|
|
|
|
def restricted_terminal(args: dict[str, Any], **kwargs: Any) -> str:
|
|
return _restricted_terminal_handler(
|
|
profile,
|
|
set(contract["toolset"]["terminal_commands"]),
|
|
args,
|
|
**kwargs,
|
|
)
|
|
|
|
restricted_terminal._livingip_no_send_handler = f"livingip-nosend-terminal:{profile.resolve()}" # type: ignore[attr-defined]
|
|
terminal_entry.handler = restricted_terminal
|
|
terminal_entry.schema = _restricted_terminal_schema(contract["toolset"]["terminal_commands"])
|
|
terminal_entry.check_fn = None
|
|
terminal_entry.requires_env = []
|
|
terminal_entry.toolset = toolset_name
|
|
for name, entry in retained.items():
|
|
entry.toolset = toolset_name
|
|
if name != "terminal":
|
|
entry.check_fn = None
|
|
entry.requires_env = []
|
|
registry._tools = _SealedToolMap(retained)
|
|
registry._toolset_checks = {}
|
|
return _assert_tool_registry(profile, contract)
|
|
|
|
|
|
def _prepare(
|
|
profile: Path,
|
|
*,
|
|
template: bool,
|
|
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any], dict[str, Any]]:
|
|
contract = _contract()
|
|
_validate_environment(contract)
|
|
_validate_profile(profile, contract, template=template)
|
|
os.environ["HERMES_HOME"] = str(profile)
|
|
hermes_source = _runtime_root() / "hermes-agent"
|
|
_require(hermes_source.is_dir(), "bound Hermes source tree is missing")
|
|
if str(hermes_source) not in sys.path:
|
|
sys.path.insert(0, str(hermes_source))
|
|
config = _load_profile_config(profile, contract)
|
|
effective_policy = _load_effective_policy(contract)
|
|
tool_surface = _prepare_tool_surface(profile, config, contract)
|
|
return contract, config, tool_surface, effective_policy
|
|
|
|
|
|
async def _exercise_runner(runner: Any, profile: Path, contract: dict[str, Any]) -> dict[str, Any]:
|
|
started = await runner.start()
|
|
_require(started, "Hermes no-send gateway did not start")
|
|
post_start = _assert_tool_registry(profile, contract)
|
|
hooks = [
|
|
{"name": item.get("name"), "path": item.get("path"), "events": sorted(item.get("events") or [])}
|
|
for item in runner.hooks.loaded_hooks
|
|
]
|
|
_require(
|
|
hooks == [{"name": "boot-md", "path": "(builtin)", "events": ["gateway:startup"]}],
|
|
"effective gateway hook inventory is not exact",
|
|
)
|
|
importlib.import_module("model_tools")
|
|
post_discovery = _assert_tool_registry(profile, contract)
|
|
await runner.stop()
|
|
_require(not runner.adapters, "gateway retained a transport adapter after stop")
|
|
return {
|
|
"started": True,
|
|
"stopped": True,
|
|
"post_start_registry": post_start,
|
|
"post_discovery_registry": post_discovery,
|
|
"gateway_hooks": hooks,
|
|
}
|
|
|
|
|
|
def probe(profile: Path, *, template: bool) -> dict[str, Any]:
|
|
contract, config, tool_surface, effective_policy = _prepare(profile, template=template)
|
|
from gateway.config import GatewayConfig
|
|
from gateway.run import GatewayRunner
|
|
|
|
gateway_config = GatewayConfig.from_dict({"platforms": {}})
|
|
runner = GatewayRunner(gateway_config)
|
|
adapters = getattr(runner, "adapters", None)
|
|
_require(isinstance(adapters, dict), "GatewayRunner adapters are not inspectable")
|
|
_require(not adapters, "GatewayRunner unexpectedly has a transport adapter")
|
|
_require(getattr(runner, "_smart_model_routing", None) == {}, "GatewayRunner retained smart-model routing")
|
|
_require(not gateway_config.get_connected_platforms(), "a messaging platform is connected")
|
|
_assert_tool_registry(profile, contract)
|
|
lifecycle = asyncio.run(_exercise_runner(runner, profile, contract))
|
|
return {
|
|
"schema": "livingip.leocleanNoSendRuntimeProbe.v1",
|
|
"status": "pass",
|
|
"runtime_mode": contract["runtime_mode"],
|
|
"config_sha256": _sha256(profile / "config.yaml"),
|
|
"gateway_runner": f"{runner.__class__.__module__}.{runner.__class__.__name__}",
|
|
"gateway_adapter_count": len(adapters),
|
|
"connected_platforms": [],
|
|
"tool_surface": tool_surface,
|
|
"effective_policy": effective_policy,
|
|
"lifecycle": lifecycle,
|
|
"provider_credential_names_present": sorted(
|
|
name for name in contract["environment"]["allowed_provider_credentials"] if os.getenv(name)
|
|
),
|
|
"claim_ceiling": contract["claim_ceiling"],
|
|
"model": config["model"]["default"],
|
|
"python_version": platform.python_version(),
|
|
}
|
|
|
|
|
|
async def _run_service(profile: Path) -> bool:
|
|
contract, _config, _surface, _effective_policy = _prepare(profile, template=False)
|
|
from gateway.config import GatewayConfig
|
|
from gateway.run import GatewayRunner
|
|
|
|
config = GatewayConfig.from_dict({"platforms": {}})
|
|
runner = GatewayRunner(config)
|
|
_require(not runner.adapters, "service cannot start with a transport adapter")
|
|
_require(getattr(runner, "_smart_model_routing", None) == {}, "service retained smart-model routing")
|
|
_assert_tool_registry(profile, contract)
|
|
importlib.import_module("model_tools")
|
|
_assert_tool_registry(profile, contract)
|
|
|
|
loop = asyncio.get_running_loop()
|
|
stop_tasks: set[asyncio.Task[Any]] = set()
|
|
|
|
def stop() -> None:
|
|
task = asyncio.create_task(runner.stop())
|
|
stop_tasks.add(task)
|
|
task.add_done_callback(stop_tasks.discard)
|
|
|
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
try:
|
|
loop.add_signal_handler(sig, stop)
|
|
except NotImplementedError:
|
|
pass
|
|
started = await runner.start()
|
|
_require(started, "Hermes no-send gateway did not start")
|
|
_assert_tool_registry(profile, contract)
|
|
await runner.wait_for_shutdown()
|
|
return True
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
for name in ("probe", "service"):
|
|
child = subparsers.add_parser(name)
|
|
child.add_argument("--profile", required=True, type=Path)
|
|
if name == "probe":
|
|
child.add_argument("--template", action="store_true")
|
|
args = parser.parse_args()
|
|
try:
|
|
if args.command == "probe":
|
|
print(json.dumps(probe(args.profile.resolve(), template=args.template), sort_keys=True))
|
|
return 0
|
|
return 0 if asyncio.run(_run_service(args.profile.resolve())) else 1
|
|
except NoSendContractError as exc:
|
|
print(json.dumps({"status": "fail", "error": str(exc)}, sort_keys=True), file=sys.stderr)
|
|
return 65
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|