Enforce DB-composed Leo responses before delivery (#116)
Some checks are pending
CI / lint-and-test (push) Waiting to run

This commit is contained in:
twentyOne2x 2026-07-13 13:27:54 +02:00 committed by GitHub
parent 54b0829b98
commit 2264ad1f2b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 997 additions and 28 deletions

View file

@ -19,6 +19,8 @@ AGENT_STATE_DIR="/opt/teleo-eval/ops/agent-state"
LEOCLEAN_BIN_DIR="/home/teleo/.hermes/profiles/leoclean/bin"
LEOCLEAN_SKILLS_DIR="/home/teleo/.hermes/profiles/leoclean/skills"
LEOCLEAN_PLUGINS_DIR="/home/teleo/.hermes/profiles/leoclean/plugins"
HERMES_AGENT_DIR="/home/teleo/.hermes/hermes-agent"
HERMES_PATCH_DIR="/home/teleo/.hermes/teleo-runtime-patches"
SYSTEMD_DIR="/etc/systemd/system"
STAMP_FILE="/opt/teleo-eval/.last-deploy-sha"
LOG_TAG="auto-deploy"
@ -53,6 +55,26 @@ NEW_SHA=$(git rev-parse "$DEPLOY_REMOTE/main")
OLD_SHA=$(cat "$STAMP_FILE" 2>/dev/null || echo "none")
if [ "$NEW_SHA" = "$OLD_SHA" ]; then
# Hermes can be upgraded independently of this repository. Repair an
# overwritten response-transform hook even when no infrastructure commit
# changed, and restart only when the patch was actually reapplied.
if [ -f "$HERMES_PATCH_DIR/apply_response_transform_hook.py" ] && [ -f "$HERMES_AGENT_DIR/run_agent.py" ]; then
if ! PATCH_RESULT=$(python3 "$HERMES_PATCH_DIR/apply_response_transform_hook.py" "$HERMES_AGENT_DIR/run_agent.py"); then
log "ERROR: Hermes response-transform drift could not be repaired"
exit 1
fi
if echo "$PATCH_RESULT" | grep -q '"status": "installed_now"'; then
log "Hermes response-transform drift repaired: $PATCH_RESULT"
if systemctl is-active --quiet leoclean-gateway.service; then
sudo systemctl restart leoclean-gateway
sleep 10
if ! systemctl is-active --quiet leoclean-gateway.service; then
log "ERROR: leoclean-gateway failed after response-transform drift repair"
exit 1
fi
fi
fi
fi
exit 0
fi
@ -81,7 +103,7 @@ fi
# Syntax check all Python files before copying
ERRORS=0
for f in lib/*.py *.py diagnostics/*.py telegram/*.py tests/*.py hermes-agent/leoclean-plugins/vps/*/*.py; do
for f in lib/*.py *.py diagnostics/*.py telegram/*.py tests/*.py hermes-agent/leoclean-plugins/vps/*/*.py hermes-agent/patches/*.py; do
[ -f "$f" ] || continue
if ! python3 -c "import ast, sys; ast.parse(open(sys.argv[1]).read())" "$f" 2>&1; then
log "SYNTAX ERROR: $f"
@ -120,12 +142,21 @@ if [ -d hermes-agent/leoclean-plugins/vps ]; then
mkdir -p "$LEOCLEAN_PLUGINS_DIR"
rsync "${RSYNC_OPTS[@]}" hermes-agent/leoclean-plugins/vps/ "$LEOCLEAN_PLUGINS_DIR/"
fi
if [ -d hermes-agent/patches ]; then
mkdir -p "$HERMES_PATCH_DIR"
rsync "${RSYNC_OPTS[@]}" hermes-agent/patches/ "$HERMES_PATCH_DIR/"
fi
[ -f research/research-session.sh ] && rsync "${RSYNC_OPTS[@]}" research/research-session.sh /opt/teleo-eval/research-session.sh
if [ -f "$HERMES_PATCH_DIR/apply_response_transform_hook.py" ]; then
PATCH_RESULT=$(python3 "$HERMES_PATCH_DIR/apply_response_transform_hook.py" "$HERMES_AGENT_DIR/run_agent.py")
log "Hermes response transform: $PATCH_RESULT"
fi
# Safety net: ensure synced .sh files are executable after rsync.
# Keep this bounded to deploy-owned paths: /opt/teleo-eval also contains
# backups and generated state that may be unreadable by the deploy user.
for dir in "$PIPELINE_DIR" "$TELEGRAM_DIR" "$DIAGNOSTICS_DIR" "$AGENT_STATE_DIR" "$LEOCLEAN_BIN_DIR" "$LEOCLEAN_SKILLS_DIR" "$LEOCLEAN_PLUGINS_DIR"; do
for dir in "$PIPELINE_DIR" "$TELEGRAM_DIR" "$DIAGNOSTICS_DIR" "$AGENT_STATE_DIR" "$LEOCLEAN_BIN_DIR" "$LEOCLEAN_SKILLS_DIR" "$LEOCLEAN_PLUGINS_DIR" "$HERMES_PATCH_DIR"; do
[ -d "$dir" ] || continue
find "$dir" -maxdepth 3 -name '*.sh' -not -perm -u+x -exec chmod +x {} +
done
@ -181,7 +212,7 @@ if [ "$OLD_SHA" != "none" ]; then
if git diff --name-only "$OLD_SHA" "$NEW_SHA" -- diagnostics/ 2>/dev/null | grep -q '\.py$'; then
add_restart teleo-diagnostics
fi
if git diff --name-only "$OLD_SHA" "$NEW_SHA" -- hermes-agent/leoclean-bin/ hermes-agent/leoclean-skills/vps/ hermes-agent/leoclean-plugins/vps/ 2>/dev/null \
if git diff --name-only "$OLD_SHA" "$NEW_SHA" -- hermes-agent/leoclean-bin/ hermes-agent/leoclean-skills/vps/ hermes-agent/leoclean-plugins/vps/ hermes-agent/patches/ 2>/dev/null \
| bash deploy/leoclean-gateway-restart-required.sh; then
add_restart_if_unit_active leoclean-gateway
fi
@ -208,6 +239,14 @@ if [ -n "$RESTART" ]; then
journalctl -u "$svc" -n 5 --no-pager 2>/dev/null || true
FAIL=1
fi
if [ "$svc" = "leoclean-gateway" ] && [ -f "$HERMES_PATCH_DIR/apply_response_transform_hook.py" ]; then
if PATCH_CHECK=$(python3 "$HERMES_PATCH_DIR/apply_response_transform_hook.py" --check "$HERMES_AGENT_DIR/run_agent.py"); then
log "leoclean-gateway response transform: $PATCH_CHECK"
else
log "ERROR: leoclean-gateway response transform check failed: $PATCH_CHECK"
FAIL=1
fi
fi
done
if echo "$RESTART" | grep -q "teleo-pipeline"; then

View file

@ -13,6 +13,8 @@ VPS_AGENT_STATE="/opt/teleo-eval/ops/agent-state"
VPS_LEOCLEAN_BIN="/home/teleo/.hermes/profiles/leoclean/bin"
VPS_LEOCLEAN_SKILLS="/home/teleo/.hermes/profiles/leoclean/skills"
VPS_LEOCLEAN_PLUGINS="/home/teleo/.hermes/profiles/leoclean/plugins"
VPS_HERMES_AGENT="/home/teleo/.hermes/hermes-agent"
VPS_HERMES_PATCHES="/home/teleo/.hermes/teleo-runtime-patches"
VPS_SYSTEMD="/etc/systemd/system"
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
@ -46,7 +48,7 @@ echo ""
# Syntax check all Python files before deploying
echo "=== Pre-deploy syntax check ==="
ERRORS=0
for f in "$REPO_ROOT/lib/"*.py "$REPO_ROOT/"*.py "$REPO_ROOT/diagnostics/"*.py "$REPO_ROOT/telegram/"*.py "$REPO_ROOT/hermes-agent/leoclean-plugins/vps/"*/*.py; do
for f in "$REPO_ROOT/lib/"*.py "$REPO_ROOT/"*.py "$REPO_ROOT/diagnostics/"*.py "$REPO_ROOT/telegram/"*.py "$REPO_ROOT/hermes-agent/leoclean-plugins/vps/"*/*.py "$REPO_ROOT/hermes-agent/patches/"*.py; do
[ -f "$f" ] || continue
if ! python3 -c "import ast, sys; ast.parse(open(sys.argv[1]).read())" "$f" 2>/dev/null; then
echo "SYNTAX ERROR: $f"
@ -106,6 +108,16 @@ echo "=== Leoclean plugins ==="
rsync "${RSYNC_OPTS[@]}" "$REPO_ROOT/hermes-agent/leoclean-plugins/vps/" "$VPS_HOST:$VPS_LEOCLEAN_PLUGINS/"
echo ""
echo "=== Hermes runtime patches ==="
if $DRY_RUN; then
echo "Would sync hermes-agent/patches/ and verify the installed response-transform hook."
else
ssh "$VPS_HOST" "mkdir -p '$VPS_HERMES_PATCHES'"
rsync "${RSYNC_OPTS[@]}" "$REPO_ROOT/hermes-agent/patches/" "$VPS_HOST:$VPS_HERMES_PATCHES/"
ssh "$VPS_HOST" "python3 '$VPS_HERMES_PATCHES/apply_response_transform_hook.py' '$VPS_HERMES_AGENT/run_agent.py'"
fi
echo ""
echo "=== Research session ==="
rsync "${RSYNC_OPTS[@]}" "$REPO_ROOT/research/research-session.sh" "$VPS_HOST:/opt/teleo-eval/research-session.sh"
echo ""

View file

@ -17,6 +17,9 @@ while IFS= read -r path || [ -n "$path" ]; do
hermes-agent/leoclean-plugins/vps/*)
restart_required=0
;;
hermes-agent/patches/*)
restart_required=0
;;
esac
done

View file

@ -0,0 +1,47 @@
{
"database_context_trace_ok": true,
"db_counts_changed": false,
"generated_at_utc": "2026-07-13T11:01:33.666438+00:00",
"mode": "working_leo_oos06_post_budget_score",
"posted_to_telegram": false,
"production_db_apply_ran": false,
"safe_pass": false,
"score": {
"behavioral_rule_schema_issues": [],
"broad_semantic_issues": [],
"concepts": {
"behavioral_rule_storage": true,
"heterogeneous_types": true,
"receipt": true,
"reviewed_policy_apply": false,
"staged_review_apply": true
},
"current_schema_overclaims": [
"reasoning_tools_unshipped_fields"
],
"custom_signals": {},
"dimension": "database_composition_judgment",
"invalid_count_invariant_detected": false,
"legacy_pass": false,
"legacy_signals": {
"canonical_db": true,
"caveat_retention": false,
"next_action": false,
"no_overclaim": true,
"staging_or_review": false
},
"max_response_words": 220,
"overclaim_detected": false,
"pass": false,
"prompt_id": "OOS-06",
"proposal_readiness_issues": [],
"response_issue_detected": false,
"response_too_long": false,
"source_evidence_semantic_issues": [],
"source_intake_issues": [],
"word_count": 210
},
"service_unchanged": true,
"source_results_json": "docs/reports/leo-working-state-20260709/telegram-handler-m3taversal-oos-oos06-post-budget-20260713.json",
"temp_profile_removed": true
}

View file

@ -0,0 +1,115 @@
{
"before_service": {
"ActiveState": "active",
"ExecMainStartTimestamp": "Mon 2026-07-13 11:00:09 UTC",
"MainPID": "3643400",
"NRestarts": "0",
"SubState": "running"
},
"changed_live_profile": false,
"database_context_all_ok": true,
"database_context_injection_count": 1,
"database_context_trace": [
{
"contract_ids": [
"reply_budget",
"mixed_packet_composition"
],
"contract_sha256": "e93a0c035e7d4084a197d71f11bf293c695024b05944cae01d58a57eb0d595bf",
"generated_at_utc": "2026-07-13T11:01:17.101126+00:00",
"injected": true,
"query_sha256": "13c255fda3a35cd913ba92960bcda1b4dbcf55a38f3301bdc2a74a8eb040df23",
"source": "kb_tool.py --local context",
"status": "ok"
}
],
"db_counts_after": {
"kb_stage.kb_proposals": 26,
"public.claim_edges": 4916,
"public.claim_evidence": 4670,
"public.claims": 1837,
"public.sources": 4145
},
"db_counts_before": {
"kb_stage.kb_proposals": 26,
"public.claim_edges": 4916,
"public.claim_evidence": 4670,
"public.claims": 1837,
"public.sources": 4145
},
"db_counts_changed": false,
"generated_at_utc": "2026-07-13T11:01:14.011205+00:00",
"handler": {
"authorized": true,
"session_key": "agent:main:telegram:group:-5146042086:9070919",
"temp_profile": "/tmp/leo-direct-claim-handler-nqtplnkt/profile"
},
"harness_notes": [
"Unchanged OOS-06 prompt after live table semantics and compact reply contract.",
"The harness uses a temporary copy of the leoclean profile and does not post to Telegram.",
"This proves handler-level live VPS GatewayRunner behavior, not human-visible Telegram delivery."
],
"mode": "live_vps_gatewayrunner_temp_profile_oos06_post_budget",
"mutates_kb_by_harness": false,
"pass_runtime": true,
"posted_to_telegram": false,
"remote_report_path": "/tmp/leo-oos06-post-budget-294b5bc74e32.json",
"remote_returncode": 0,
"remote_run_id": "294b5bc74e32",
"results": [
{
"claim_ceiling": "Live VPS GatewayRunner reply from temp leoclean profile; no Telegram post; no production DB apply authorized.",
"database_context_trace": [
{
"contract_ids": [
"reply_budget",
"mixed_packet_composition"
],
"contract_sha256": "e93a0c035e7d4084a197d71f11bf293c695024b05944cae01d58a57eb0d595bf",
"generated_at_utc": "2026-07-13T11:01:17.101126+00:00",
"injected": true,
"query_sha256": "13c255fda3a35cd913ba92960bcda1b4dbcf55a38f3301bdc2a74a8eb040df23",
"source": "kb_tool.py --local context",
"status": "ok"
}
],
"dimension": "database_composition_judgment",
"ended_at_utc": "2026-07-13T11:01:30.146238+00:00",
"evidence_tier": "live_vps_gatewayrunner_temp_profile",
"mutates_kb": false,
"ok": true,
"prompt": "Suppose a new research packet contains factual observations, a strategic framework, a disputed interpretation, a governance rule, and a correction to an old belief. How should Leo compose the database so it can reason over the packet later without flattening it all into claims? Give the review and apply sequence too, but do not write anything now.",
"prompt_id": "OOS-06",
"reply": "Mapping across the five elements:\n\n- Factual observations \u2192 claims (type: empirical, sources + evidence rows attached). Strategic framework \u2192 reasoning_tools (name, description, category; no scope field; no claim-edge pointing at it). Disputed interpretation \u2192 claims (type: empirical or structural, lower confidence, dispute surfaced via claim_edges between the competing claim rows). Governance rule \u2192 behavioral_rules if it is an operating rule (rule, rationale, category, rank, agent_id); only promote to governance_gates if it carries explicit evaluation criteria, an evidence bar, and a pass_condition. Correction to an old belief \u2192 beliefs row update (status: revised, new statement + confidence + falsifier; the old row's status moves to revised, not deleted).\n\n- Review/apply sequence: stage the full packet as a typed pending_review proposal without touching canonical tables; reviewer checks type assignments, evidence attachments, edge endpoints (claims only, never reasoning_tool or belief IDs), and exact row values; apply only the supported collections (claims, sources, evidence, edges, reasoning_tools, belief updates) after explicit authorization; keep behavioral_rules and governance_gates staged \u2014 approve_claim does not cover them, separate capability required; record applied_at on the proposal receipt plus created/updated row IDs and run a postflight readback.\n\n- Hard boundary: behavioral_rules and governance_gates cannot be applied in the same pass as claims even if the authorization is broad \u2014 they remain staged until a reviewed apply path for those tables exists.",
"started_at_utc": "2026-07-13T11:01:14.476172+00:00",
"turn": 1
}
],
"service_before_after": {
"after": {
"ActiveState": "active",
"ExecMainStartTimestamp": "Mon 2026-07-13 11:00:09 UTC",
"MainPID": "3643400",
"NRestarts": "0",
"SubState": "running"
},
"before": {
"ActiveState": "active",
"ExecMainStartTimestamp": "Mon 2026-07-13 11:00:09 UTC",
"MainPID": "3643400",
"NRestarts": "0",
"SubState": "running"
},
"unchanged_from_preexisting_live_readback": true
},
"source": {
"chat_id": "-5146042086",
"chat_name": "Leo",
"chat_type": "group",
"platform": "telegram",
"user_id": "9070919",
"user_name": "codex handler direct claim"
},
"source_report_path": "docs/reports/leo-working-state-20260709/telegram-handler-m3taversal-oos-oos06-post-budget-20260713.json",
"temp_profile_removed": true
}

View file

@ -493,6 +493,62 @@ def operational_contracts(
return contracts
def human_join(values: list[str]) -> str:
if not values:
return ""
if len(values) == 1:
return values[0]
if len(values) == 2:
return f"{values[0]} and {values[1]}"
return f"{', '.join(values[:-1])}, and {values[-1]}"
def compile_operational_response(contracts: list[dict[str, Any]]) -> str | None:
"""Compile a safe response when a model draft violates live DB contracts."""
by_id = {str(contract.get("id") or ""): contract for contract in contracts}
mixed = by_id.get("mixed_packet_composition")
if mixed:
mapping = mixed.get("map") or {}
supported = human_join([str(item) for item in mixed.get("approve_claim_supported") or []])
staged = human_join([str(item) for item in mixed.get("keep_staged_until_separate_capability") or []])
return (
f"- Map factual observations and disputed interpretations to {mapping.get('facts_and_disputes')}; keep "
"competing claims queryable through claim-to-claim edges. Store the strategic framework in "
f"{mapping.get('reusable_framework')}. Put the governance rule in {mapping.get('operating_rule')} using "
"rule, rationale, category, rank, "
"and agent_id unless it defines criteria, an evidence_bar, and a pass_condition, in which case use "
f"{mapping.get('evaluative_gate')}. Stage the correction to the old belief as a "
f"{mapping.get('agent_position')} update; claim_edges never point to "
"reasoning_tools or beliefs.\n\n"
"- Stage one typed pending_review proposal with exact rows and no canonical writes. Review mappings, "
"evidence, claim-only edge endpoints, and unsupported changes. After explicit authorization, "
f"approve_claim applies only {supported}. approve_claim "
"supports neither behavioral_rules nor governance_gates; those tables, belief updates, and existing-row "
f"updates remain staged until a separate reviewed apply capability exists. The contract keeps {staged} "
"outside this apply.\n\n"
"- Receipt: proposal-level applied_at, created or updated row IDs, and a postflight readback. Until those "
"exist, staged or approved content is not live canonical knowledge."
)
source = by_id.get("source_intake")
if source:
safe_steps = [str(item) for item in source.get("safe_without_apply_authorization") or []]
retain_step = safe_steps[0] if safe_steps else "retain the artifact plus its locator and content hash"
stage_step = safe_steps[1] if len(safe_steps) > 1 else "stage candidates in a pending_review proposal"
return (
f"- {source.get('required_lead')} Immediately {retain_step}. A filename, chat label, attachment reference, or "
"source_ref is not source identity.\n\n"
f"- {stage_step}. Deduplicate candidate claims and retain evidence excerpts and contradictions. Do not "
"create public.sources or other canonical rows during capture or staging.\n\n"
f"- Explicit approval begins {source.get('approval_begins')}. The receipt should include the retained "
"locator and hash, proposal ID and status, reviewed payload, proposal-level applied_at, created row IDs, "
"and postflight readback. Until apply succeeds, the material is staged rather than live canonical knowledge."
)
return None
def load_current_public_schema(args: argparse.Namespace, table_names: set[str]) -> dict[str, list[str]]:
if not table_names:
return {}
@ -1217,6 +1273,7 @@ def load_edges(args: argparse.Namespace, claim_ids: list[str], limit: int) -> di
def bundle(args: argparse.Namespace, query: str, claim_limit: int, context_limit: int) -> dict[str, Any]:
contracts = context_operational_contracts(args, query)
claims = find_claims(args, query, claim_limit)
claim_ids = [claim["id"] for claim in claims]
evidence = load_evidence(args, claim_ids, 4)
@ -1224,7 +1281,8 @@ def bundle(args: argparse.Namespace, query: str, claim_limit: int, context_limit
return {
"query": query,
"terms": query_terms(query),
"operational_contracts": context_operational_contracts(args, query),
"operational_contracts": contracts,
"compiled_response": compile_operational_response(contracts),
"claims": [
{
**claim,

View file

@ -5,14 +5,22 @@ from __future__ import annotations
import hashlib
import json
import os
import re
import subprocess
import sys
import threading
from collections import OrderedDict
from collections.abc import Callable
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable
from typing import Any
DEFAULT_TIMEOUT_SECONDS = 10
MAX_QUERY_CHARS = 16_000
MAX_PENDING_SNAPSHOTS = 128
_SNAPSHOT_LOCK = threading.Lock()
_PENDING_SNAPSHOTS: OrderedDict[tuple[str, str], dict[str, Any]] = OrderedDict()
def _trace(record: dict[str, Any]) -> None:
@ -54,13 +62,31 @@ def _format_context(contracts: list[dict[str, Any]]) -> str:
)
def build_database_context(
def _snapshot_key(session_id: str, query_sha256: str) -> tuple[str, str]:
return (str(session_id or ""), query_sha256)
def _store_snapshot(session_id: str, snapshot: dict[str, Any]) -> None:
key = _snapshot_key(session_id, str(snapshot["query_sha256"]))
with _SNAPSHOT_LOCK:
_PENDING_SNAPSHOTS[key] = snapshot
_PENDING_SNAPSHOTS.move_to_end(key)
while len(_PENDING_SNAPSHOTS) > MAX_PENDING_SNAPSHOTS:
_PENDING_SNAPSHOTS.popitem(last=False)
def _take_snapshot(session_id: str, query_sha256: str) -> dict[str, Any] | None:
with _SNAPSHOT_LOCK:
return _PENDING_SNAPSHOTS.pop(_snapshot_key(session_id, query_sha256), None)
def _load_database_snapshot(
user_message: str,
*,
hermes_home: Path | None = None,
runner: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
) -> str:
"""Return current operational contracts for one exact operator question."""
) -> dict[str, Any]:
"""Load one immutable DB-contract snapshot for generation and validation."""
query = str(user_message or "")[:MAX_QUERY_CHARS]
home = hermes_home or Path(os.getenv("HERMES_HOME", str(Path.home() / ".hermes")))
@ -68,6 +94,7 @@ def build_database_context(
query_sha256 = hashlib.sha256(query.encode("utf-8")).hexdigest()
base_record: dict[str, Any] = {
"generated_at_utc": datetime.now(timezone.utc).isoformat(),
"event": "pre_llm_call",
"query_sha256": query_sha256,
"source": "kb_tool.py --local context",
}
@ -75,7 +102,13 @@ def build_database_context(
if not kb_tool.is_file():
record = base_record | {"status": "error", "error": "kb_tool_missing", "injected": True}
_trace(record)
return _failure_context("kb_tool_missing")
return {
"status": "error",
"query_sha256": query_sha256,
"contracts": [],
"compiled_response": None,
"context": _failure_context("kb_tool_missing"),
}
timeout = int(os.getenv("LEO_DB_CONTEXT_TIMEOUT_SECONDS", str(DEFAULT_TIMEOUT_SECONDS)))
command = [
@ -96,11 +129,23 @@ def build_database_context(
except subprocess.TimeoutExpired:
record = base_record | {"status": "error", "error": "timeout", "injected": True}
_trace(record)
return _failure_context("timeout")
return {
"status": "error",
"query_sha256": query_sha256,
"contracts": [],
"compiled_response": None,
"context": _failure_context("timeout"),
}
except OSError as exc:
record = base_record | {"status": "error", "error": type(exc).__name__, "injected": True}
_trace(record)
return _failure_context(type(exc).__name__)
return {
"status": "error",
"query_sha256": query_sha256,
"contracts": [],
"compiled_response": None,
"context": _failure_context(type(exc).__name__),
}
if completed.returncode != 0:
record = base_record | {
@ -109,17 +154,32 @@ def build_database_context(
"injected": True,
}
_trace(record)
return _failure_context(f"kb_tool_exit_{completed.returncode}")
return {
"status": "error",
"query_sha256": query_sha256,
"contracts": [],
"compiled_response": None,
"context": _failure_context(f"kb_tool_exit_{completed.returncode}"),
}
try:
payload = json.loads(completed.stdout)
contracts = payload.get("operational_contracts")
if not isinstance(contracts, list) or not all(isinstance(item, dict) for item in contracts):
raise ValueError("operational_contracts_missing")
compiled_response = payload.get("compiled_response")
if compiled_response is not None and not isinstance(compiled_response, str):
raise ValueError("compiled_response_invalid")
except (json.JSONDecodeError, TypeError, ValueError) as exc:
record = base_record | {"status": "error", "error": str(exc), "injected": True}
_trace(record)
return _failure_context(str(exc))
return {
"status": "error",
"query_sha256": query_sha256,
"contracts": [],
"compiled_response": None,
"context": _failure_context(str(exc)),
}
contract_json = json.dumps(contracts, sort_keys=True, separators=(",", ":"))
record = base_record | {
@ -127,14 +187,192 @@ def build_database_context(
"injected": True,
"contract_ids": [str(item.get("id") or "") for item in contracts],
"contract_sha256": hashlib.sha256(contract_json.encode("utf-8")).hexdigest(),
"compiled_response_available": bool(compiled_response),
}
_trace(record)
return _format_context(contracts)
return {
"status": "ok",
"query_sha256": query_sha256,
"contracts": contracts,
"compiled_response": compiled_response,
"context": _format_context(contracts),
}
def build_database_context(
user_message: str,
*,
hermes_home: Path | None = None,
runner: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
) -> str:
"""Return current operational contracts for one exact operator question."""
return str(
_load_database_snapshot(user_message, hermes_home=hermes_home, runner=runner)["context"]
)
def _word_count(value: str) -> int:
return len(re.findall(r"\b\w+(?:[-']\w+)*\b", value))
def _reply_hard_max(contracts: list[dict[str, Any]]) -> int | None:
for contract in contracts:
if contract.get("id") == "reply_budget" and isinstance(contract.get("hard_max_words"), int):
return int(contract["hard_max_words"])
return None
def _mixed_packet_issues(response: str) -> list[str]:
lowered = response.lower()
issues: list[str] = []
required_terms = {
"claims": "claim",
"sources": "source",
"evidence": "evidence",
"reasoning_tools": "reasoning_tools",
"behavioral_rules": "behavioral_rules",
"governance_gates": "governance_gates",
"beliefs": "belief",
"staging": "stage",
"approve_claim": "approve_claim",
}
for label, term in required_terms.items():
if term not in lowered:
issues.append(f"missing_{label}")
if not re.search(r"reviewed apply|review and authoriz|review.{0,50}apply", response, re.I | re.S):
issues.append("missing_reviewed_apply_boundary")
if not ("applied_at" in lowered and re.search(r"readback|postflight", response, re.I)):
issues.append("missing_apply_receipt")
if not re.search(
r"approve_claim.{0,220}(?:supports neither|does not (?:support|cover|apply)|cannot apply|applies only)",
response,
re.I | re.S,
):
issues.append("missing_approve_claim_boundary")
if re.search(
r"supported collections?\s*\([^)]*(?:belief|behavioral_rules|governance_gates|existing[- ]row)",
response,
re.I | re.S,
):
issues.append("unsupported_collection_listed_as_supported")
if re.search(
r"(?:approve_claim\s+)?(?:applies?|apply) only[^.;\n]{0,220}"
r"(?:belief updates?|behavioral_rules|governance_gates|existing[- ]row updates?)",
response,
re.I | re.S,
):
issues.append("unsupported_collection_listed_in_apply")
for segment in re.split(r"(?<=[.!?])\s+|\n+", response):
negative = re.search(r"\b(?:no|not|never|without|must not|do not|does not|cannot)\b", segment, re.I)
if re.search(r"reasoning_tools?.{0,100}\bscope\b", segment, re.I) and not negative:
issues.append("reasoning_tools_scope_overclaim")
if re.search(r"claims?.{0,100}\bfalsifier\b", segment, re.I) and not negative:
issues.append("claims_falsifier_overclaim")
if re.search(r"behavioral_rules?.{0,100}\bstatus\b", segment, re.I) and not negative:
issues.append("behavioral_rules_status_overclaim")
if (
re.search(r"claim_edges?.{0,140}(?:reasoning_tools?|beliefs?)", segment, re.I | re.S)
and re.search(r"point|connect|link|endpoint", segment, re.I)
and not negative
):
issues.append("non_claim_edge_endpoint")
return sorted(set(issues))
def _source_intake_issues(response: str) -> list[str]:
lowered = response.lower()
issues: list[str] = []
if not re.search(r"url|storage_path|storage path|content hash|file hash", response, re.I):
issues.append("missing_real_source_identity")
if not re.search(r"pending_review|pending review|proposal", response, re.I):
issues.append("missing_staging")
if not re.search(r"approval|authoriz", response, re.I) or "apply" not in lowered:
issues.append("missing_apply_boundary")
if not re.search(r"live.{0,80}(?:not shipped|not live|unavailable)", response, re.I | re.S):
issues.append("missing_live_intake_ceiling")
if re.search(
r"public\.sources.{0,120}(?:author|title|publisher|publication date|published_at)",
response,
re.I | re.S,
):
issues.append("unshipped_source_fields")
if re.search(
r"(?:create|write|insert).{0,80}public\.sources.{0,100}(?:before|during).{0,80}(?:review|staging)",
response,
re.I | re.S,
):
issues.append("canonical_source_before_review")
return sorted(set(issues))
def response_contract_issues(response: str, contracts: list[dict[str, Any]]) -> list[str]:
contract_ids = {str(contract.get("id") or "") for contract in contracts}
issues: list[str] = []
hard_max = _reply_hard_max(contracts)
if hard_max is not None and _word_count(response) > hard_max:
issues.append("reply_budget_exceeded")
if "mixed_packet_composition" in contract_ids:
issues.extend(_mixed_packet_issues(response))
elif "source_intake" in contract_ids:
issues.extend(_source_intake_issues(response))
return sorted(set(issues))
def _pre_llm_call(**kwargs: Any) -> dict[str, str]:
return {"context": build_database_context(str(kwargs.get("user_message") or ""))}
user_message = str(kwargs.get("user_message") or "")
snapshot = _load_database_snapshot(user_message)
_store_snapshot(str(kwargs.get("session_id") or ""), snapshot)
return {"context": str(snapshot["context"])}
def _post_llm_call(**kwargs: Any) -> dict[str, str] | None:
user_message = str(kwargs.get("user_message") or "")[:MAX_QUERY_CHARS]
query_sha256 = hashlib.sha256(user_message.encode("utf-8")).hexdigest()
snapshot = _take_snapshot(str(kwargs.get("session_id") or ""), query_sha256)
response = str(kwargs.get("assistant_response") or "")
base_record = {
"generated_at_utc": datetime.now(timezone.utc).isoformat(),
"event": "post_llm_call",
"query_sha256": query_sha256,
"response_sha256": hashlib.sha256(response.encode("utf-8")).hexdigest(),
"validated": True,
}
if snapshot is None:
_trace(base_record | {"status": "error", "error": "contract_snapshot_missing", "transformed": False})
return None
if snapshot.get("status") != "ok":
_trace(base_record | {"status": "skipped", "reason": "contract_lookup_failed", "transformed": False})
return None
contracts = list(snapshot.get("contracts") or [])
issues = response_contract_issues(response, contracts)
compiled_response = snapshot.get("compiled_response")
compiled_issues = (
response_contract_issues(str(compiled_response), contracts) if isinstance(compiled_response, str) else []
)
transformed = bool(issues and isinstance(compiled_response, str) and not compiled_issues)
_trace(
base_record
| {
"status": "ok" if not issues or transformed else "error",
"contract_ids": [str(item.get("id") or "") for item in contracts],
"issues": issues,
"compiled_response_issues": compiled_issues,
"transformed": transformed,
"delivered_response_sha256": hashlib.sha256(
(str(compiled_response) if transformed else response).encode("utf-8")
).hexdigest(),
}
)
if transformed:
return {"assistant_response": str(compiled_response)}
return None
def register(ctx: Any) -> None:
ctx.register_hook("pre_llm_call", _pre_llm_call)
ctx.register_hook("post_llm_call", _post_llm_call)

View file

@ -1,6 +1,7 @@
name: leo-db-context
version: 1.0.0
description: Inject query-specific read-only Teleo database contracts before each Leo model turn.
version: 1.1.0
description: Inject and enforce query-specific read-only Teleo database contracts for each Leo model turn.
author: living-ip
provides_hooks:
- pre_llm_call
- post_llm_call

View file

@ -104,6 +104,14 @@ Treat the emitted `Current Runtime Contracts` / `operational_contracts` block as
binding deployed-code readback. Follow its `required_lead`, schema guards,
capability tier, and reply budget. It overrides recalled or generic architecture.
The profile's `leo-db-context` plugin automatically uses one immutable contract
snapshot for generation and pre-delivery validation. When a covered draft
violates the snapshot, `kb_tool.py` compiles the delivered response from the
snapshot's mappings, apply capability, staged boundary, and receipt fields, and
Hermes persists that replacement before sending it. Repair the contract,
compiler, or validator when this path fails; do not add answer examples as a
substitute.
Use narrower bridge commands when needed:
```bash

View file

@ -100,6 +100,16 @@ Treat the emitted `Current Runtime Contracts` / `operational_contracts` block as
binding deployed-code readback. Follow its `required_lead`, schema guards,
capability tier, and reply budget. It overrides recalled or generic architecture.
The live leoclean profile injects that exact read-only contract automatically
before the model turn. Before delivery, the same snapshot validates the draft.
For contract-covered composition and source-intake questions, an invalid draft
is replaced by a response compiled from that snapshot's mappings, supported
collections, staged collections, and receipt requirements. The replacement is
written into session memory before Telegram delivery, so remembered and visible
answers stay identical. This is database/runtime composition, not repeated
prompt training; do not work around a failed validator by adding examples to
the skill.
Use only the bounded follow-ups relevant to the question:
```bash

View file

@ -0,0 +1,151 @@
#!/usr/bin/env python3
"""Install the Hermes post-LLM response transform before persistence."""
from __future__ import annotations
import argparse
import ast
import hashlib
import json
import os
import tempfile
from pathlib import Path
PATCH_VERSION = "teleo-response-transform-hook-v1"
PATCH_MARKER = " # TELEO_RESPONSE_TRANSFORM_HOOK_V1\n"
START_ANCHOR = " # Save trajectory if enabled\n"
END_ANCHOR = " # Extract reasoning from the last assistant message (if any)\n"
REPLACEMENT = ''' # TELEO_RESPONSE_TRANSFORM_HOOK_V1
# Run response validators before trajectory/session persistence. A plugin
# may return {"assistant_response": "..."}; the last non-empty value wins.
# This keeps delivered text and durable conversation memory identical.
if final_response and not interrupted:
try:
from hermes_cli.plugins import invoke_hook as _invoke_hook
_post_results = _invoke_hook(
"post_llm_call",
session_id=self.session_id,
user_message=original_user_message,
assistant_response=final_response,
conversation_history=list(messages),
model=self.model,
platform=getattr(self, "platform", None) or "",
)
_original_final_response = final_response
for _post_result in _post_results:
if not isinstance(_post_result, dict):
continue
_candidate = _post_result.get("assistant_response")
if isinstance(_candidate, str) and _candidate.strip():
final_response = _candidate.strip()
if final_response != _original_final_response:
_updated_assistant_message = False
for _message in reversed(messages):
if _message.get("role") == "assistant" and not _message.get("tool_calls"):
_message["content"] = final_response
_updated_assistant_message = True
break
if not _updated_assistant_message:
messages.append({"role": "assistant", "content": final_response})
except Exception as exc:
logger.warning("post_llm_call hook failed: %s", exc)
# Save trajectory if enabled
self._save_trajectory(messages, user_message, completed)
# Clean up VM and browser for this task after conversation completes
self._cleanup_task_resources(effective_task_id)
# Persist session to both JSON log and SQLite
self._persist_session(messages, conversation_history)
'''
def sha256_text(value: str) -> str:
return hashlib.sha256(value.encode("utf-8")).hexdigest()
def patch_source(source: str) -> tuple[str, bool]:
if PATCH_MARKER in source:
required = (
'_candidate = _post_result.get("assistant_response")',
"self._save_trajectory(messages, user_message, completed)",
"self._persist_session(messages, conversation_history)",
)
missing = [item for item in required if item not in source]
if missing:
raise ValueError(f"installed patch marker is incomplete: {missing}")
return source, False
if source.count(START_ANCHOR) != 1 or source.count(END_ANCHOR) != 1:
raise ValueError("Hermes response lifecycle anchors changed; refusing an unreviewed patch")
start = source.index(START_ANCHOR)
end = source.index(END_ANCHOR, start)
original_block = source[start:end]
required_original = (
'"post_llm_call"',
"self._save_trajectory(messages, user_message, completed)",
"self._cleanup_task_resources(effective_task_id)",
"self._persist_session(messages, conversation_history)",
)
missing = [item for item in required_original if item not in original_block]
if missing:
raise ValueError(f"Hermes response lifecycle changed; missing expected statements: {missing}")
patched = source[:start] + REPLACEMENT + source[end:]
ast.parse(patched)
return patched, True
def install(path: Path, *, check: bool) -> tuple[dict[str, str | bool], int]:
source = path.read_text(encoding="utf-8")
patched, changed = patch_source(source)
before_sha = sha256_text(source)
after_sha = sha256_text(patched)
if check:
status = "patch_required" if changed else "installed"
return {
"patch_version": PATCH_VERSION,
"status": status,
"target": str(path),
"sha256": before_sha,
}, 1 if changed else 0
if changed:
mode = path.stat().st_mode & 0o777
with tempfile.NamedTemporaryFile(
"w", encoding="utf-8", dir=path.parent, prefix=f".{path.name}.", delete=False
) as handle:
handle.write(patched)
temp_path = Path(handle.name)
try:
temp_path.chmod(mode)
os.replace(temp_path, path)
finally:
temp_path.unlink(missing_ok=True)
ast.parse(path.read_text(encoding="utf-8"))
return {
"patch_version": PATCH_VERSION,
"status": "installed_now" if changed else "already_installed",
"target": str(path),
"before_sha256": before_sha,
"after_sha256": after_sha,
}, 0
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("target", type=Path)
parser.add_argument("--check", action="store_true")
args = parser.parse_args()
result, code = install(args.target, check=args.check)
print(json.dumps(result, sort_keys=True))
return code
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -315,10 +315,24 @@ async def run_suite():
temp_removed = remove_tree(tmp_root)
report["temp_profile_removed"] = temp_removed
report["database_context_trace"] = database_context_trace
report["database_context_injection_count"] = len(report["database_context_trace"])
report["database_context_all_ok"] = bool(report["database_context_trace"]) and all(
context_injections = [
item for item in database_context_trace if item.get("event", "pre_llm_call") == "pre_llm_call"
]
response_validations = [
item for item in database_context_trace if item.get("event") == "post_llm_call"
]
report["database_context_injection_count"] = len(context_injections)
report["database_context_all_ok"] = bool(context_injections) and all(
item.get("status") == "ok" and item.get("injected") is True
for item in report["database_context_trace"]
for item in context_injections
)
report["database_response_validation_count"] = len(response_validations)
report["database_response_validation_all_ok"] = bool(response_validations) and all(
item.get("status") == "ok" and item.get("validated") is True
for item in response_validations
)
report["database_response_transform_count"] = sum(
item.get("transformed") is True for item in response_validations
)
write_report_snapshot(report)

View file

@ -33,6 +33,9 @@ def write_score_markdown(path: Path, report: dict[str, Any]) -> None:
f"Temporary profile removed: `{report['temp_profile_removed']}`",
f"Database context injections: `{report['database_context_injection_count']}`",
f"Database context all OK: `{report['database_context_all_ok']}`",
f"Database response validations: `{report['database_response_validation_count']}`",
f"Database response validation all OK: `{report['database_response_validation_all_ok']}`",
f"Database-composed replacements: `{report['database_response_transform_count']}`",
f"Posted to Telegram: `{report['posted_to_telegram']}`",
"",
"## Prompt Scores",
@ -68,6 +71,9 @@ def build_score_report(report: dict[str, Any], *, memory_token: str) -> dict[str
"temp_profile_removed": report.get("temp_profile_removed"),
"database_context_injection_count": report.get("database_context_injection_count"),
"database_context_all_ok": report.get("database_context_all_ok"),
"database_response_validation_count": report.get("database_response_validation_count"),
"database_response_validation_all_ok": report.get("database_response_validation_all_ok"),
"database_response_transform_count": report.get("database_response_transform_count"),
"posted_to_telegram": report.get("posted_to_telegram"),
"production_db_apply_ran": False,
}
@ -82,6 +88,8 @@ def score_report_passes(report: dict[str, Any], score_report: dict[str, Any]) ->
and report.get("temp_profile_removed") is True
and report.get("database_context_all_ok") is True
and int(report.get("database_context_injection_count") or 0) >= len(report.get("results") or [])
and report.get("database_response_validation_all_ok") is True
and int(report.get("database_response_validation_count") or 0) >= len(report.get("results") or [])
and report.get("posted_to_telegram") is False
and score_report["score"]["pass"]
)

View file

@ -327,12 +327,12 @@ CONCEPT_PATTERNS: dict[str, tuple[re.Pattern[str], ...]] = {
re.compile(r"behavioral_rules", re.I),
re.compile(r"governance_gates", re.I),
re.compile(
r"does not (?:accept|insert|support)|supports neither|neither.{0,80}nor|"
r"does not (?:accept|insert|support|cover)|supports neither|neither.{0,80}nor|"
r"(?:behavioral_rules|governance_gates).{0,120}cannot be applied by approve_claim|"
r"approve_claim.{0,120}cannot apply",
re.I | re.S,
),
re.compile(r"separate.{0,50}reviewed apply|reviewed.{0,50}apply capability", re.I | re.S),
re.compile(r"separate.{0,50}reviewed apply|reviewed.{0,50}apply (?:capability|path)", re.I | re.S),
),
"runtime_inputs": (
re.compile(r"Postgres|canonical (?:DB|database|counts?)|(?:DB|database) totals?", re.I),
@ -471,7 +471,8 @@ COUNT_INVARIANT_REJECTION_RE = re.compile(
SCHEMA_GAP_QUALIFIER_RE = re.compile(
r"\b(?:proposed|future|not current|not shipped|does not exist|doesn't exist|absent|"
r"has no|have no|no column|not an edge|would require|schema gap|must be added|"
r"does not support|doesn't support|supports neither|not supported|must not|do not invent)\b",
r"does not support|doesn't support|supports neither|not supported|must not|do not invent)\b|"
r"\bno\s+(?:[a-z_]+\s+){0,3}(?:column|field|table)\b",
re.I,
)
CURRENT_SCHEMA_ASSERTION_PATTERNS: dict[str, re.Pattern[str]] = {
@ -576,6 +577,12 @@ ROW_LEVEL_APPLIED_AT_RE = re.compile(
r"each (?:step|row|insert).{0,60}(?:gets?|has|requires?).{0,30}(?:non-null )?applied_at",
re.I | re.S,
)
UNSUPPORTED_COMPOSITION_APPLY_RE = re.compile(
r"supported collections?\s*\([^)]*(?:belief|behavioral_rules|governance_gates|existing[- ]row)|"
r"(?:approve_claim\s+)?(?:applies?|apply) only[^.;\n]{0,220}"
r"(?:belief updates?|behavioral_rules|governance_gates|existing[- ]row updates?)",
re.I | re.S,
)
APPROVED_APPLY_ACTION_RE = re.compile(
r"approved.{0,240}(?:guarded )?apply|(?:guarded )?apply.{0,240}approved",
re.I | re.S,
@ -700,6 +707,14 @@ def source_intake_issues(prompt_id: str, reply: str) -> list[str]:
return []
def composition_capability_issues(prompt_id: str, reply: str) -> list[str]:
if prompt_id != "OOS-06":
return []
if UNSUPPORTED_COMPOSITION_APPLY_RE.search(reply):
return ["unsupported_collection_listed_as_applyable"]
return []
def score_reply(prompt: dict[str, Any], reply: str, *, memory_token: str) -> dict[str, Any]:
legacy_score = base.score_reply(prompt, reply)
concepts = {concept: matched_concept(reply, concept) for concept in prompt["required_concepts"]}
@ -733,6 +748,7 @@ def score_reply(prompt: dict[str, Any], reply: str, *, memory_token: str) -> dic
semantic_issues = broad_semantic_issues(reply)
readiness_issues = proposal_readiness_issues(prompt["id"], reply)
intake_issues = source_intake_issues(prompt["id"], reply)
composition_issues = composition_capability_issues(prompt["id"], reply)
word_count = len(re.findall(r"\b\w+(?:[-']\w+)*\b", reply))
max_response_words = MAX_RESPONSE_WORDS.get(prompt["id"], DEFAULT_MAX_RESPONSE_WORDS)
response_too_long = word_count > max_response_words
@ -752,6 +768,7 @@ def score_reply(prompt: dict[str, Any], reply: str, *, memory_token: str) -> dic
"broad_semantic_issues": semantic_issues,
"proposal_readiness_issues": readiness_issues,
"source_intake_issues": intake_issues,
"composition_capability_issues": composition_issues,
"word_count": word_count,
"max_response_words": max_response_words,
"response_too_long": response_too_long,
@ -766,6 +783,7 @@ def score_reply(prompt: dict[str, Any], reply: str, *, memory_token: str) -> dic
and not semantic_issues
and not readiness_issues
and not intake_issues
and not composition_issues
and not response_too_long
),
}

View file

@ -20,12 +20,11 @@ def load_plugin():
return module
def test_plugin_registers_pre_llm_call_hook() -> None:
def test_plugin_registers_generation_and_delivery_hooks() -> None:
module = load_plugin()
registered = []
module.register(SimpleNamespace(register_hook=lambda name, callback: registered.append((name, callback))))
assert len(registered) == 1
assert registered[0][0] == "pre_llm_call"
assert [name for name, _callback in registered] == ["pre_llm_call", "post_llm_call"]
def test_plugin_injects_only_operational_contracts_and_writes_redacted_trace(
@ -45,6 +44,7 @@ def test_plugin_injects_only_operational_contracts_and_writes_redacted_trace(
{"id": "reply_budget", "hard_max_words": 220},
{"id": "source_intake", "capability_tier": "build-local compiler only"},
],
"compiled_response": "must not be injected either",
}
calls = []
@ -71,6 +71,92 @@ def test_plugin_injects_only_operational_contracts_and_writes_redacted_trace(
assert kwargs["timeout"] == module.DEFAULT_TIMEOUT_SECONDS
def test_plugin_replaces_contract_violating_draft_with_compiled_db_response(
tmp_path: Path, monkeypatch
) -> None:
module = load_plugin()
home = tmp_path / "profile"
kb_tool = home / "bin" / "kb_tool.py"
kb_tool.parent.mkdir(parents=True)
kb_tool.write_text("# fixture\n", encoding="utf-8")
trace = tmp_path / "trace.jsonl"
monkeypatch.setenv("HERMES_HOME", str(home))
monkeypatch.setenv("LEO_DB_CONTEXT_TRACE_PATH", str(trace))
compiled = (
"Map claims with sources and evidence; put the framework in reasoning_tools, the rule in "
"behavioral_rules, an evaluative gate in governance_gates, and stage the belief correction. "
"Stage one pending_review proposal. approve_claim applies only claims, sources, evidence, edges, and "
"reasoning_tools; approve_claim supports neither behavioral_rules nor governance_gates. Belief updates "
"remain staged until a separate reviewed apply capability exists. Receipt: proposal applied_at and "
"postflight row readback."
)
payload = {
"operational_contracts": [
{"id": "reply_budget", "hard_max_words": 200},
{"id": "mixed_packet_composition"},
],
"compiled_response": compiled,
}
def fake_runner(command, **_kwargs):
return subprocess.CompletedProcess(command, 0, stdout=json.dumps(payload), stderr="")
snapshot = module._load_database_snapshot("compose this packet", hermes_home=home, runner=fake_runner)
module._store_snapshot("session-1", snapshot)
result = module._post_llm_call(
session_id="session-1",
user_message="compose this packet",
assistant_response=(
"Claims, sources, evidence, reasoning_tools, behavioral_rules, governance_gates, and beliefs. "
"Stage and review a proposal, then apply only the supported collections (claims, sources, evidence, "
"edges, reasoning_tools, belief updates). approve_claim does not cover behavioral_rules or "
"governance_gates; use a separate reviewed apply path. Receipt: applied_at and postflight readback."
),
)
assert result == {"assistant_response": compiled}
records = [json.loads(line) for line in trace.read_text(encoding="utf-8").splitlines()]
assert records[-1]["event"] == "post_llm_call"
assert records[-1]["transformed"] is True
assert records[-1]["issues"] == [
"unsupported_collection_listed_as_supported",
"unsupported_collection_listed_in_apply",
]
assert "compose this packet" not in trace.read_text(encoding="utf-8")
def test_plugin_keeps_contract_valid_draft(tmp_path: Path) -> None:
module = load_plugin()
home = tmp_path / "profile"
kb_tool = home / "bin" / "kb_tool.py"
kb_tool.parent.mkdir(parents=True)
kb_tool.write_text("# fixture\n", encoding="utf-8")
valid = (
"Map claims with sources and evidence; put the framework in reasoning_tools, the rule in "
"behavioral_rules, an evaluative gate in governance_gates, and stage the belief correction. "
"Stage one pending_review proposal. approve_claim applies only claims, sources, evidence, edges, and "
"reasoning_tools; approve_claim supports neither behavioral_rules nor governance_gates. Belief updates "
"remain staged until a separate reviewed apply capability exists. Receipt: proposal applied_at and "
"postflight row readback."
)
payload = {
"operational_contracts": [
{"id": "reply_budget", "hard_max_words": 200},
{"id": "mixed_packet_composition"},
],
"compiled_response": valid,
}
def fake_runner(command, **_kwargs):
return subprocess.CompletedProcess(command, 0, stdout=json.dumps(payload), stderr="")
snapshot = module._load_database_snapshot("compose this packet", hermes_home=home, runner=fake_runner)
module._store_snapshot("session-2", snapshot)
assert module._post_llm_call(
session_id="session-2", user_message="compose this packet", assistant_response=valid
) is None
def test_plugin_fails_closed_when_database_contract_lookup_fails(tmp_path: Path) -> None:
module = load_plugin()
home = tmp_path / "profile"
@ -93,3 +179,6 @@ def test_handler_harness_retains_database_context_proof_fields() -> None:
assert '"database_context_trace"' in source
assert '"database_context_injection_count"' in source
assert '"database_context_all_ok"' in source
assert '"database_response_validation_count"' in source
assert '"database_response_validation_all_ok"' in source
assert '"database_response_transform_count"' in source

View file

@ -235,6 +235,23 @@ def test_vps_bridge_operational_contracts_are_query_specific_and_schema_exact()
assert {"reply_budget", "runtime_persistence"} <= runtime_ids
def test_vps_bridge_compiles_safe_mixed_packet_response_from_contracts() -> None:
module = _load_module(BRIDGE_DIR / "kb_tool.py")
contracts = module.operational_contracts(
"Compose a packet with a factual observation, strategic framework, disputed interpretation, governance "
"rule, and old belief."
)
response = module.compile_operational_response(contracts)
assert response is not None
assert len(re.findall(r"\b\w+(?:[-']\w+)*\b", response)) <= 200
assert "approve_claim applies only claims, sources, evidence, edges, and reasoning_tools" in response
assert "approve_claim supports neither behavioral_rules nor governance_gates" in response
assert "belief updates, and existing-row updates remain staged" in response
assert "proposal-level applied_at" in response
def test_vps_bridge_context_markdown_surfaces_runtime_contracts(capsys) -> None:
module = _load_module(BRIDGE_DIR / "kb_tool.py")
data = {

View file

@ -0,0 +1,88 @@
"""Tests for the managed Hermes response-transform lifecycle patch."""
from __future__ import annotations
import importlib.util
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
PATCHER = ROOT / "hermes-agent" / "patches" / "apply_response_transform_hook.py"
def load_patcher():
spec = importlib.util.spec_from_file_location("hermes_response_transform_patcher", PATCHER)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def upstream_fixture() -> str:
return '''class Agent:
def run(self, messages, user_message, completed, effective_task_id, conversation_history,
final_response, interrupted, original_user_message):
# Save trajectory if enabled
self._save_trajectory(messages, user_message, completed)
# Clean up VM and browser for this task after conversation completes
self._cleanup_task_resources(effective_task_id)
# Persist session to both JSON log and SQLite
self._persist_session(messages, conversation_history)
# Plugin hook: post_llm_call
if final_response and not interrupted:
from hermes_cli.plugins import invoke_hook as _invoke_hook
_invoke_hook(
"post_llm_call",
session_id=self.session_id,
user_message=original_user_message,
assistant_response=final_response,
)
# Extract reasoning from the last assistant message (if any)
last_reasoning = None
return final_response, last_reasoning
'''
def test_patch_moves_response_transform_before_persistence_and_is_idempotent() -> None:
module = load_patcher()
patched, changed = module.patch_source(upstream_fixture())
assert changed is True
assert module.PATCH_MARKER in patched
assert patched.index('_candidate = _post_result.get("assistant_response")') < patched.index(
"self._save_trajectory(messages, user_message, completed)"
)
assert patched.index("self._save_trajectory(messages, user_message, completed)") < patched.index(
"self._persist_session(messages, conversation_history)"
)
assert module.patch_source(patched) == (patched, False)
def test_patch_refuses_unknown_hermes_lifecycle() -> None:
module = load_patcher()
with pytest.raises(ValueError, match="anchors changed"):
module.patch_source("class Agent:\n pass\n")
def test_install_check_reports_patch_required_then_installed(tmp_path: Path) -> None:
module = load_patcher()
target = tmp_path / "run_agent.py"
target.write_text(upstream_fixture(), encoding="utf-8")
before, before_code = module.install(target, check=True)
assert before_code == 1
assert before["status"] == "patch_required"
applied, applied_code = module.install(target, check=False)
assert applied_code == 0
assert applied["status"] == "installed_now"
after, after_code = module.install(target, check=True)
assert after_code == 0
assert after["status"] == "installed"

View file

@ -1,7 +1,6 @@
import subprocess
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
@ -104,6 +103,9 @@ def test_deploy_scripts_sync_leoclean_skills_and_restart_gateway_for_runtime_cha
assert 'hermes-agent/leoclean-bin/' in auto_deploy
assert 'hermes-agent/leoclean-skills/vps/' in auto_deploy
assert 'hermes-agent/leoclean-plugins/vps/' in auto_deploy
assert 'HERMES_PATCH_DIR="/home/teleo/.hermes/teleo-runtime-patches"' in auto_deploy
assert '"status": "installed_now"' in auto_deploy
assert 'Hermes response-transform drift repaired' in auto_deploy
assert 'deploy/leoclean-gateway-restart-required.sh' in auto_deploy
assert 'TELEO_AUTO_DEPLOY_REEXECED' in auto_deploy
assert 'exec bash "$DEPLOY_CHECKOUT/deploy/auto-deploy.sh"' in auto_deploy
@ -118,6 +120,7 @@ def test_deploy_scripts_sync_leoclean_skills_and_restart_gateway_for_runtime_cha
assert 'hermes-agent/leoclean-bin/' in manual_deploy
assert 'hermes-agent/leoclean-skills/vps/' in manual_deploy
assert 'hermes-agent/leoclean-plugins/vps/' in manual_deploy
assert 'VPS_HERMES_PATCHES="/home/teleo/.hermes/teleo-runtime-patches"' in manual_deploy
assert 'systemctl is-active --quiet leoclean-gateway.service' in manual_deploy
@ -138,6 +141,9 @@ def test_auto_deploy_restarts_gateway_for_leoclean_runtime_changes():
assert _leoclean_gateway_restart_required(
"hermes-agent/leoclean-plugins/vps/leo-db-context/__init__.py"
)
assert _leoclean_gateway_restart_required(
"hermes-agent/patches/apply_response_transform_hook.py"
)
def test_auto_deploy_sudoers_installer_is_root_only_and_visudo_checked():

View file

@ -2,6 +2,7 @@
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
@ -13,6 +14,15 @@ import run_leo_direct_claim_handler_suite as handler # noqa: E402
import working_leo_m3taversal_oos_benchmark as benchmark # noqa: E402
def load_kb_tool():
path = REPO_ROOT / "hermes-agent" / "leoclean-bin" / "kb_tool.py"
spec = importlib.util.spec_from_file_location("kb_tool_for_oos_test", path)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def good_reply(prompt_id: str, token: str) -> str:
common = (
"DB readback: claims: 1837; sources: 4145; claim_edges: 4916; claim_evidence: 4670; "
@ -209,6 +219,11 @@ def test_oos_schema_guard_allows_explicit_future_schema_gap() -> None:
assert benchmark.current_schema_overclaims(reply) == []
def test_oos_schema_guard_allows_compact_no_field_wording() -> None:
reply = "public.reasoning_tools stores name and description; no scope field exists in the current table."
assert benchmark.current_schema_overclaims(reply) == []
def test_oos_schema_guard_distinguishes_superseded_by_column_from_edge_type() -> None:
valid = (
"Create a supersedes edge from the new claim to the old claim, then set the old claim's "
@ -461,6 +476,38 @@ def test_oos_reviewed_policy_apply_accepts_cannot_be_applied_wording() -> None:
assert all(pattern.search(reply) for pattern in patterns)
def test_oos_reviewed_policy_apply_accepts_does_not_cover_and_path_wording() -> None:
patterns = benchmark.CONCEPT_PATTERNS["reviewed_policy_apply"]
reply = (
"behavioral_rules and governance_gates remain staged: approve_claim does not cover them, and both require "
"a separate reviewed apply path."
)
assert all(pattern.search(reply) for pattern in patterns)
def test_oos_composition_rejects_belief_update_in_supported_apply_list() -> None:
reply = (
"Stage a proposal, review it, then apply only the supported collections "
"(claims, sources, evidence, edges, reasoning_tools, belief updates). "
"behavioral_rules and governance_gates remain staged for a separate reviewed apply capability."
)
assert benchmark.composition_capability_issues("OOS-06", reply) == [
"unsupported_collection_listed_as_applyable"
]
def test_db_compiled_responses_pass_composition_and_source_intake_benchmarks() -> None:
kb_tool = load_kb_tool()
token = "demo-ledger-deadbeef"
prompts = {prompt["id"]: prompt for prompt in benchmark.prompt_catalog(token)}
for prompt_id in ("OOS-06", "OOS-14"):
prompt = prompts[prompt_id]
response = kb_tool.compile_operational_response(kb_tool.operational_contracts(prompt["message"]))
assert response is not None
assert benchmark.score_reply(prompt, response, memory_token=token)["pass"] is True
def test_oos_source_intake_accepts_live_not_shipped_wording() -> None:
patterns = benchmark.CONCEPT_PATTERNS["bounded_intake_tier"]
reply = "The local compiler is proven; live VPS/chat intake is not shipped."