Keep Leo candidate reasoning grounded and stop automatic chat reinforcement (#180)
Some checks are pending
CI / lint-and-test (push) Waiting to run
Some checks are pending
CI / lint-and-test (push) Waiting to run
* Preserve candidate reasoning and disable automatic chat reinforcement * Retain exact-row guards for combined claim audits
This commit is contained in:
parent
77e1336c0f
commit
415669c2e4
10 changed files with 326 additions and 14 deletions
|
|
@ -19,8 +19,10 @@ AGENT_STATE_DIR="/opt/teleo-eval/ops/agent-state"
|
||||||
LEOCLEAN_BIN_DIR="/home/teleo/.hermes/profiles/leoclean/bin"
|
LEOCLEAN_BIN_DIR="/home/teleo/.hermes/profiles/leoclean/bin"
|
||||||
LEOCLEAN_SKILLS_DIR="/home/teleo/.hermes/profiles/leoclean/skills"
|
LEOCLEAN_SKILLS_DIR="/home/teleo/.hermes/profiles/leoclean/skills"
|
||||||
LEOCLEAN_PLUGINS_DIR="/home/teleo/.hermes/profiles/leoclean/plugins"
|
LEOCLEAN_PLUGINS_DIR="/home/teleo/.hermes/profiles/leoclean/plugins"
|
||||||
|
LEOCLEAN_CONFIG="/home/teleo/.hermes/profiles/leoclean/config.yaml"
|
||||||
HERMES_AGENT_DIR="/home/teleo/.hermes/hermes-agent"
|
HERMES_AGENT_DIR="/home/teleo/.hermes/hermes-agent"
|
||||||
HERMES_PATCH_DIR="/home/teleo/.hermes/teleo-runtime-patches"
|
HERMES_PATCH_DIR="/home/teleo/.hermes/teleo-runtime-patches"
|
||||||
|
LEOCLEAN_POLICY_PATCH="$HERMES_PATCH_DIR/enforce_leoclean_learning_policy.py"
|
||||||
SYSTEMD_DIR="/etc/systemd/system"
|
SYSTEMD_DIR="/etc/systemd/system"
|
||||||
STAMP_FILE="/opt/teleo-eval/.last-deploy-sha"
|
STAMP_FILE="/opt/teleo-eval/.last-deploy-sha"
|
||||||
LOG_TAG="auto-deploy"
|
LOG_TAG="auto-deploy"
|
||||||
|
|
@ -55,6 +57,7 @@ NEW_SHA=$(git rev-parse "$DEPLOY_REMOTE/main")
|
||||||
OLD_SHA=$(cat "$STAMP_FILE" 2>/dev/null || echo "none")
|
OLD_SHA=$(cat "$STAMP_FILE" 2>/dev/null || echo "none")
|
||||||
|
|
||||||
if [ "$NEW_SHA" = "$OLD_SHA" ]; then
|
if [ "$NEW_SHA" = "$OLD_SHA" ]; then
|
||||||
|
LEOCLEAN_DRIFT_REPAIRED=false
|
||||||
# Hermes can be upgraded independently of this repository. Repair an
|
# Hermes can be upgraded independently of this repository. Repair an
|
||||||
# overwritten response-transform hook even when no infrastructure commit
|
# overwritten response-transform hook even when no infrastructure commit
|
||||||
# changed, and restart only when the patch was actually reapplied.
|
# changed, and restart only when the patch was actually reapplied.
|
||||||
|
|
@ -65,14 +68,25 @@ if [ "$NEW_SHA" = "$OLD_SHA" ]; then
|
||||||
fi
|
fi
|
||||||
if echo "$PATCH_RESULT" | grep -q '"status": "installed_now"'; then
|
if echo "$PATCH_RESULT" | grep -q '"status": "installed_now"'; then
|
||||||
log "Hermes response-transform drift repaired: $PATCH_RESULT"
|
log "Hermes response-transform drift repaired: $PATCH_RESULT"
|
||||||
if systemctl is-active --quiet leoclean-gateway.service; then
|
LEOCLEAN_DRIFT_REPAIRED=true
|
||||||
sudo systemctl restart leoclean-gateway
|
fi
|
||||||
sleep 10
|
fi
|
||||||
if ! systemctl is-active --quiet leoclean-gateway.service; then
|
if [ -f "$LEOCLEAN_POLICY_PATCH" ] && [ -f "$LEOCLEAN_CONFIG" ]; then
|
||||||
log "ERROR: leoclean-gateway failed after response-transform drift repair"
|
if ! POLICY_RESULT=$(python3 "$LEOCLEAN_POLICY_PATCH" "$LEOCLEAN_CONFIG"); then
|
||||||
exit 1
|
log "ERROR: leoclean learning-policy drift could not be repaired"
|
||||||
fi
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
if echo "$POLICY_RESULT" | grep -q '"status": "updated"'; then
|
||||||
|
log "Leoclean learning-policy drift repaired: $POLICY_RESULT"
|
||||||
|
LEOCLEAN_DRIFT_REPAIRED=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if $LEOCLEAN_DRIFT_REPAIRED && 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 runtime-policy drift repair"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
exit 0
|
exit 0
|
||||||
|
|
@ -152,6 +166,14 @@ 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")
|
PATCH_RESULT=$(python3 "$HERMES_PATCH_DIR/apply_response_transform_hook.py" "$HERMES_AGENT_DIR/run_agent.py")
|
||||||
log "Hermes response transform: $PATCH_RESULT"
|
log "Hermes response transform: $PATCH_RESULT"
|
||||||
fi
|
fi
|
||||||
|
LEOCLEAN_POLICY_CHANGED=false
|
||||||
|
if [ -f "$LEOCLEAN_POLICY_PATCH" ] && [ -f "$LEOCLEAN_CONFIG" ]; then
|
||||||
|
POLICY_RESULT=$(python3 "$LEOCLEAN_POLICY_PATCH" "$LEOCLEAN_CONFIG")
|
||||||
|
log "Leoclean learning policy: $POLICY_RESULT"
|
||||||
|
if echo "$POLICY_RESULT" | grep -q '"status": "updated"'; then
|
||||||
|
LEOCLEAN_POLICY_CHANGED=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Safety net: ensure synced .sh files are executable after rsync.
|
# Safety net: ensure synced .sh files are executable after rsync.
|
||||||
# Keep this bounded to deploy-owned paths: /opt/teleo-eval also contains
|
# Keep this bounded to deploy-owned paths: /opt/teleo-eval also contains
|
||||||
|
|
@ -216,6 +238,9 @@ if [ "$OLD_SHA" != "none" ]; then
|
||||||
| bash deploy/leoclean-gateway-restart-required.sh; then
|
| bash deploy/leoclean-gateway-restart-required.sh; then
|
||||||
add_restart_if_unit_active leoclean-gateway
|
add_restart_if_unit_active leoclean-gateway
|
||||||
fi
|
fi
|
||||||
|
if $LEOCLEAN_POLICY_CHANGED; then
|
||||||
|
add_restart_if_unit_active leoclean-gateway
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
RESTART="teleo-pipeline teleo-diagnostics"
|
RESTART="teleo-pipeline teleo-diagnostics"
|
||||||
add_restart_if_unit_active teleo-agent@leo
|
add_restart_if_unit_active teleo-agent@leo
|
||||||
|
|
@ -247,6 +272,14 @@ if [ -n "$RESTART" ]; then
|
||||||
FAIL=1
|
FAIL=1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
if [ "$svc" = "leoclean-gateway" ] && [ -f "$LEOCLEAN_POLICY_PATCH" ]; then
|
||||||
|
if POLICY_CHECK=$(python3 "$LEOCLEAN_POLICY_PATCH" --check "$LEOCLEAN_CONFIG"); then
|
||||||
|
log "leoclean-gateway learning policy: $POLICY_CHECK"
|
||||||
|
else
|
||||||
|
log "ERROR: leoclean-gateway learning policy check failed: $POLICY_CHECK"
|
||||||
|
FAIL=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if echo "$RESTART" | grep -q "teleo-pipeline"; then
|
if echo "$RESTART" | grep -q "teleo-pipeline"; then
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,10 @@ VPS_AGENT_STATE="/opt/teleo-eval/ops/agent-state"
|
||||||
VPS_LEOCLEAN_BIN="/home/teleo/.hermes/profiles/leoclean/bin"
|
VPS_LEOCLEAN_BIN="/home/teleo/.hermes/profiles/leoclean/bin"
|
||||||
VPS_LEOCLEAN_SKILLS="/home/teleo/.hermes/profiles/leoclean/skills"
|
VPS_LEOCLEAN_SKILLS="/home/teleo/.hermes/profiles/leoclean/skills"
|
||||||
VPS_LEOCLEAN_PLUGINS="/home/teleo/.hermes/profiles/leoclean/plugins"
|
VPS_LEOCLEAN_PLUGINS="/home/teleo/.hermes/profiles/leoclean/plugins"
|
||||||
|
VPS_LEOCLEAN_CONFIG="/home/teleo/.hermes/profiles/leoclean/config.yaml"
|
||||||
VPS_HERMES_AGENT="/home/teleo/.hermes/hermes-agent"
|
VPS_HERMES_AGENT="/home/teleo/.hermes/hermes-agent"
|
||||||
VPS_HERMES_PATCHES="/home/teleo/.hermes/teleo-runtime-patches"
|
VPS_HERMES_PATCHES="/home/teleo/.hermes/teleo-runtime-patches"
|
||||||
|
VPS_LEOCLEAN_POLICY_PATCH="$VPS_HERMES_PATCHES/enforce_leoclean_learning_policy.py"
|
||||||
VPS_SYSTEMD="/etc/systemd/system"
|
VPS_SYSTEMD="/etc/systemd/system"
|
||||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
|
||||||
|
|
@ -109,12 +111,18 @@ rsync "${RSYNC_OPTS[@]}" "$REPO_ROOT/hermes-agent/leoclean-plugins/vps/" "$VPS_H
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
echo "=== Hermes runtime patches ==="
|
echo "=== Hermes runtime patches ==="
|
||||||
|
LEOCLEAN_POLICY_CHANGED=false
|
||||||
if $DRY_RUN; then
|
if $DRY_RUN; then
|
||||||
echo "Would sync hermes-agent/patches/ and verify the installed response-transform hook."
|
echo "Would sync hermes-agent/patches/, verify the response-transform hook, and enforce the leoclean learning policy."
|
||||||
else
|
else
|
||||||
ssh "$VPS_HOST" "mkdir -p '$VPS_HERMES_PATCHES'"
|
ssh "$VPS_HOST" "mkdir -p '$VPS_HERMES_PATCHES'"
|
||||||
rsync "${RSYNC_OPTS[@]}" "$REPO_ROOT/hermes-agent/patches/" "$VPS_HOST:$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'"
|
ssh "$VPS_HOST" "python3 '$VPS_HERMES_PATCHES/apply_response_transform_hook.py' '$VPS_HERMES_AGENT/run_agent.py'"
|
||||||
|
POLICY_RESULT=$(ssh "$VPS_HOST" "python3 '$VPS_LEOCLEAN_POLICY_PATCH' '$VPS_LEOCLEAN_CONFIG'")
|
||||||
|
echo "$POLICY_RESULT"
|
||||||
|
if echo "$POLICY_RESULT" | grep -q '"status": "updated"'; then
|
||||||
|
LEOCLEAN_POLICY_CHANGED=true
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|
@ -142,4 +150,9 @@ if $RESTART; then
|
||||||
echo "=== Restarting services ==="
|
echo "=== Restarting services ==="
|
||||||
ssh "$VPS_HOST" "sudo systemctl restart teleo-pipeline teleo-diagnostics; if systemctl is-active --quiet teleo-agent@leo.service; then sudo systemctl restart teleo-agent@leo; fi; if systemctl list-units --all --full teleo-agent@leo-wallet-test.service --no-legend | grep -q .; then sudo systemctl restart teleo-agent@leo-wallet-test; fi; if systemctl is-active --quiet leoclean-gateway.service; then sudo systemctl restart leoclean-gateway; fi"
|
ssh "$VPS_HOST" "sudo systemctl restart teleo-pipeline teleo-diagnostics; if systemctl is-active --quiet teleo-agent@leo.service; then sudo systemctl restart teleo-agent@leo; fi; if systemctl list-units --all --full teleo-agent@leo-wallet-test.service --no-legend | grep -q .; then sudo systemctl restart teleo-agent@leo-wallet-test; fi; if systemctl is-active --quiet leoclean-gateway.service; then sudo systemctl restart leoclean-gateway; fi"
|
||||||
echo "Services restarted."
|
echo "Services restarted."
|
||||||
|
elif $LEOCLEAN_POLICY_CHANGED; then
|
||||||
|
echo ""
|
||||||
|
echo "=== Restarting leoclean for learning-policy change ==="
|
||||||
|
ssh "$VPS_HOST" "if systemctl is-active --quiet leoclean-gateway.service; then sudo systemctl restart leoclean-gateway; fi"
|
||||||
|
echo "Leoclean gateway restarted."
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,10 @@ terminal:
|
||||||
memory:
|
memory:
|
||||||
enabled: true
|
enabled: true
|
||||||
search: sqlite_fts5
|
search: sqlite_fts5
|
||||||
|
nudge_interval: 0
|
||||||
|
|
||||||
|
skills:
|
||||||
|
creation_nudge_interval: 0
|
||||||
|
|
||||||
tools:
|
tools:
|
||||||
web_search: true
|
web_search: true
|
||||||
|
|
|
||||||
|
|
@ -587,9 +587,7 @@ def operational_contracts(
|
||||||
|
|
||||||
apply_receipt_question = any(
|
apply_receipt_question = any(
|
||||||
term in lowered for term in ("canonical receipt", "postflight", "closure proof", "before/after")
|
term in lowered for term in ("canonical receipt", "postflight", "closure proof", "before/after")
|
||||||
) or bool(
|
) or bool(re.search(r"\bapplied_at\b\s+(?:readback|proof|receipt|postflight)\b", lowered))
|
||||||
re.search(r"\bapplied_at\b\s+(?:readback|proof|receipt|postflight)\b", lowered)
|
|
||||||
)
|
|
||||||
if apply_receipt_question:
|
if apply_receipt_question:
|
||||||
contracts.append(
|
contracts.append(
|
||||||
{
|
{
|
||||||
|
|
@ -685,8 +683,26 @@ def operational_contracts(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
existing_claim_audit_question = any(
|
||||||
|
term in lowered
|
||||||
|
for term in (
|
||||||
|
"inspect one exact claim",
|
||||||
|
"exact claim body and its evidence",
|
||||||
|
"one retrieved canonical claim",
|
||||||
|
"linked source or evidence id",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
candidate_claim_composition_question = bool(
|
||||||
|
any(
|
||||||
|
term in lowered
|
||||||
|
for term in ("candidate claim", "reviewable candidate", "candidate only", "revised exact claim body")
|
||||||
|
)
|
||||||
|
and any(term in lowered for term in ("draft", "propose", "revise", "rewrite", "narrow", "return"))
|
||||||
|
and not existing_claim_audit_question
|
||||||
|
)
|
||||||
claim_evidence_challenge_question = bool(
|
claim_evidence_challenge_question = bool(
|
||||||
any(term in lowered for term in ("claim", "exact body", "claim body"))
|
not candidate_claim_composition_question
|
||||||
|
and any(term in lowered for term in ("claim", "exact body", "claim body"))
|
||||||
and any(term in lowered for term in ("evidence", "source"))
|
and any(term in lowered for term in ("evidence", "source"))
|
||||||
and any(
|
and any(
|
||||||
term in lowered
|
term in lowered
|
||||||
|
|
|
||||||
114
hermes-agent/patches/enforce_leoclean_learning_policy.py
Normal file
114
hermes-agent/patches/enforce_leoclean_learning_policy.py
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Disable automatic chat-to-memory and chat-to-skill reinforcement for leoclean."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
POLICY_VERSION = "teleo-leoclean-learning-policy-v1"
|
||||||
|
BACKUP_SUFFIX = ".before-teleo-learning-policy"
|
||||||
|
MEMORY_NUDGE_INTERVAL = 0
|
||||||
|
SKILL_CREATION_NUDGE_INTERVAL = 0
|
||||||
|
|
||||||
|
|
||||||
|
def _load_config(path: Path) -> dict[str, Any]:
|
||||||
|
if path.is_symlink():
|
||||||
|
raise ValueError("refusing symlinked profile config")
|
||||||
|
raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
raise ValueError("profile config must be a YAML mapping")
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
def policy_projection(config: dict[str, Any]) -> dict[str, int | None]:
|
||||||
|
memory = config.get("memory") if isinstance(config.get("memory"), dict) else {}
|
||||||
|
skills = config.get("skills") if isinstance(config.get("skills"), dict) else {}
|
||||||
|
return {
|
||||||
|
"memory_nudge_interval": memory.get("nudge_interval"),
|
||||||
|
"skill_creation_nudge_interval": skills.get("creation_nudge_interval"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def enforce_policy(path: Path, *, check: bool = False) -> tuple[dict[str, Any], int]:
|
||||||
|
config = _load_config(path)
|
||||||
|
before = policy_projection(config)
|
||||||
|
expected = {
|
||||||
|
"memory_nudge_interval": MEMORY_NUDGE_INTERVAL,
|
||||||
|
"skill_creation_nudge_interval": SKILL_CREATION_NUDGE_INTERVAL,
|
||||||
|
}
|
||||||
|
changed = before != expected
|
||||||
|
|
||||||
|
if check:
|
||||||
|
return {
|
||||||
|
"policy_version": POLICY_VERSION,
|
||||||
|
"status": "change_required" if changed else "compliant",
|
||||||
|
"target": str(path),
|
||||||
|
"policy": before,
|
||||||
|
}, 1 if changed else 0
|
||||||
|
|
||||||
|
backup_path = path.with_name(path.name + BACKUP_SUFFIX)
|
||||||
|
if changed:
|
||||||
|
memory = config.setdefault("memory", {})
|
||||||
|
skills = config.setdefault("skills", {})
|
||||||
|
if not isinstance(memory, dict) or not isinstance(skills, dict):
|
||||||
|
raise ValueError("memory and skills config sections must be YAML mappings")
|
||||||
|
memory["nudge_interval"] = MEMORY_NUDGE_INTERVAL
|
||||||
|
skills["creation_nudge_interval"] = SKILL_CREATION_NUDGE_INTERVAL
|
||||||
|
|
||||||
|
if not backup_path.exists():
|
||||||
|
shutil.copy2(path, backup_path)
|
||||||
|
|
||||||
|
mode = path.stat().st_mode & 0o777
|
||||||
|
rendered = yaml.safe_dump(config, sort_keys=False, allow_unicode=False)
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
"w", encoding="utf-8", dir=path.parent, prefix=f".{path.name}.", delete=False
|
||||||
|
) as handle:
|
||||||
|
handle.write(rendered)
|
||||||
|
temp_path = Path(handle.name)
|
||||||
|
try:
|
||||||
|
temp_path.chmod(mode)
|
||||||
|
os.replace(temp_path, path)
|
||||||
|
finally:
|
||||||
|
temp_path.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
after = policy_projection(_load_config(path))
|
||||||
|
if after != expected:
|
||||||
|
raise ValueError("profile config did not retain the required learning policy")
|
||||||
|
return {
|
||||||
|
"policy_version": POLICY_VERSION,
|
||||||
|
"status": "updated" if changed else "already_compliant",
|
||||||
|
"target": str(path),
|
||||||
|
"backup": str(backup_path) if changed else None,
|
||||||
|
"policy": after,
|
||||||
|
}, 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()
|
||||||
|
try:
|
||||||
|
result, code = enforce_policy(args.target, check=args.check)
|
||||||
|
except (OSError, ValueError, yaml.YAMLError) as exc:
|
||||||
|
result = {
|
||||||
|
"policy_version": POLICY_VERSION,
|
||||||
|
"status": "error",
|
||||||
|
"target": str(args.target),
|
||||||
|
"error": type(exc).__name__,
|
||||||
|
}
|
||||||
|
code = 2
|
||||||
|
print(json.dumps(result, sort_keys=True))
|
||||||
|
return code
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -235,7 +235,15 @@ def identity_config_projection(raw: dict[str, Any]) -> dict[str, Any]:
|
||||||
section_fields = {
|
section_fields = {
|
||||||
"model": ("provider", "default", "smart_routing", "smart_routing_model", "max_tokens"),
|
"model": ("provider", "default", "smart_routing", "smart_routing_model", "max_tokens"),
|
||||||
"agent": ("reasoning_effort",),
|
"agent": ("reasoning_effort",),
|
||||||
"memory": ("enabled", "search"),
|
"memory": (
|
||||||
|
"enabled",
|
||||||
|
"memory_enabled",
|
||||||
|
"user_profile_enabled",
|
||||||
|
"search",
|
||||||
|
"nudge_interval",
|
||||||
|
"flush_min_turns",
|
||||||
|
),
|
||||||
|
"skills": ("creation_nudge_interval",),
|
||||||
"tools": ("allowed", "write_enabled"),
|
"tools": ("allowed", "write_enabled"),
|
||||||
"identity_runtime": ("network_send_enabled", "database_write_enabled", "allowed_tools"),
|
"identity_runtime": ("network_send_enabled", "database_write_enabled", "allowed_tools"),
|
||||||
}
|
}
|
||||||
|
|
@ -328,6 +336,8 @@ def safe_model_config(config_path: Path) -> dict[str, Any]:
|
||||||
"reasoning_effort": _nested(identity_config, "agent", "reasoning_effort"),
|
"reasoning_effort": _nested(identity_config, "agent", "reasoning_effort"),
|
||||||
"memory_enabled": _nested(identity_config, "memory", "enabled"),
|
"memory_enabled": _nested(identity_config, "memory", "enabled"),
|
||||||
"memory_search": _nested(identity_config, "memory", "search"),
|
"memory_search": _nested(identity_config, "memory", "search"),
|
||||||
|
"automatic_memory_review_interval": _nested(identity_config, "memory", "nudge_interval"),
|
||||||
|
"automatic_skill_review_interval": _nested(identity_config, "skills", "creation_nudge_interval"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -495,6 +505,8 @@ def identity_runtime_payload(manifest: dict[str, Any]) -> dict[str, Any]:
|
||||||
"reasoning_effort",
|
"reasoning_effort",
|
||||||
"memory_enabled",
|
"memory_enabled",
|
||||||
"memory_search",
|
"memory_search",
|
||||||
|
"automatic_memory_review_interval",
|
||||||
|
"automatic_skill_review_interval",
|
||||||
"base_model_weights",
|
"base_model_weights",
|
||||||
"actual_per_turn_model_must_be_recorded_by_the_call_receipt",
|
"actual_per_turn_model_must_be_recorded_by_the_call_receipt",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
76
tests/test_enforce_leoclean_learning_policy.py
Normal file
76
tests/test_enforce_leoclean_learning_policy.py
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
PATCH_PATH = ROOT / "hermes-agent" / "patches" / "enforce_leoclean_learning_policy.py"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_module():
|
||||||
|
spec = importlib.util.spec_from_file_location("enforce_leoclean_learning_policy", PATCH_PATH)
|
||||||
|
assert spec and spec.loader
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
def test_learning_policy_disables_automatic_reviews_without_removing_explicit_tools(tmp_path: Path) -> None:
|
||||||
|
module = _load_module()
|
||||||
|
config_path = tmp_path / "config.yaml"
|
||||||
|
original = """model:
|
||||||
|
provider: openrouter
|
||||||
|
gateway:
|
||||||
|
telegram:
|
||||||
|
bot_token: private-test-token
|
||||||
|
memory:
|
||||||
|
memory_enabled: true
|
||||||
|
user_profile_enabled: true
|
||||||
|
nudge_interval: 10
|
||||||
|
skills:
|
||||||
|
external_dirs: []
|
||||||
|
creation_nudge_interval: 10
|
||||||
|
"""
|
||||||
|
config_path.write_text(original, encoding="utf-8")
|
||||||
|
config_path.chmod(0o600)
|
||||||
|
|
||||||
|
result, code = module.enforce_policy(config_path)
|
||||||
|
|
||||||
|
assert code == 0
|
||||||
|
assert result["status"] == "updated"
|
||||||
|
assert result["policy"] == {
|
||||||
|
"memory_nudge_interval": 0,
|
||||||
|
"skill_creation_nudge_interval": 0,
|
||||||
|
}
|
||||||
|
assert "private-test-token" not in json.dumps(result)
|
||||||
|
updated = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||||
|
assert updated["memory"] == {
|
||||||
|
"memory_enabled": True,
|
||||||
|
"user_profile_enabled": True,
|
||||||
|
"nudge_interval": 0,
|
||||||
|
}
|
||||||
|
assert updated["skills"] == {"external_dirs": [], "creation_nudge_interval": 0}
|
||||||
|
assert updated["gateway"]["telegram"]["bot_token"] == "private-test-token"
|
||||||
|
assert config_path.stat().st_mode & 0o777 == 0o600
|
||||||
|
assert config_path.with_name(config_path.name + module.BACKUP_SUFFIX).read_text(encoding="utf-8") == original
|
||||||
|
|
||||||
|
check_result, check_code = module.enforce_policy(config_path, check=True)
|
||||||
|
assert check_code == 0
|
||||||
|
assert check_result["status"] == "compliant"
|
||||||
|
|
||||||
|
|
||||||
|
def test_learning_policy_check_reports_drift_without_writing(tmp_path: Path) -> None:
|
||||||
|
module = _load_module()
|
||||||
|
config_path = tmp_path / "config.yaml"
|
||||||
|
original = "memory:\n memory_enabled: true\n"
|
||||||
|
config_path.write_text(original, encoding="utf-8")
|
||||||
|
|
||||||
|
result, code = module.enforce_policy(config_path, check=True)
|
||||||
|
|
||||||
|
assert code == 1
|
||||||
|
assert result["status"] == "change_required"
|
||||||
|
assert config_path.read_text(encoding="utf-8") == original
|
||||||
|
assert not config_path.with_name(config_path.name + module.BACKUP_SUFFIX).exists()
|
||||||
|
|
@ -1669,6 +1669,39 @@ def test_vps_bridge_binds_claim_challenge_to_exact_retrieved_rows() -> None:
|
||||||
assert "no database write was made" in unbound_response
|
assert "no database write was made" in unbound_response
|
||||||
|
|
||||||
|
|
||||||
|
def test_vps_bridge_does_not_replace_candidate_composition_with_retrieved_claim() -> None:
|
||||||
|
module = _load_module(BRIDGE_DIR / "kb_tool.py")
|
||||||
|
candidate_queries = [
|
||||||
|
(
|
||||||
|
"Draft the strongest reviewable candidate claim this discussion supports. Give the exact claim body, "
|
||||||
|
"initial confidence, falsifier, minimum source and evidence requirements, and relations to existing "
|
||||||
|
"canonical rows. Do not write or stage it yet."
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"That candidate still smuggles in unsupported assumptions. Narrow the claim. Return the revised exact "
|
||||||
|
"claim body, revised initial confidence, a concrete falsifier, minimum source and evidence requirements, "
|
||||||
|
"and relations to existing rows. Do not write or stage anything."
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
for query in candidate_queries:
|
||||||
|
contracts = module.operational_contracts(query)
|
||||||
|
contract_ids = {contract["id"] for contract in contracts}
|
||||||
|
|
||||||
|
assert "claim_evidence_challenge" not in contract_ids
|
||||||
|
assert module.compile_operational_response(contracts) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_vps_bridge_keeps_exact_row_guard_for_combined_audit_and_candidate_request() -> None:
|
||||||
|
module = _load_module(BRIDGE_DIR / "kb_tool.py")
|
||||||
|
contracts = module.operational_contracts(
|
||||||
|
"Inspect one exact claim body and its evidence, challenge its unsupported assumption, and then draft a "
|
||||||
|
"narrower candidate claim."
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "claim_evidence_challenge" in {contract["id"] for contract in contracts}
|
||||||
|
|
||||||
|
|
||||||
def test_vps_prepare_source_retrieves_canonical_candidates_before_model_extraction(
|
def test_vps_prepare_source_retrieves_canonical_candidates_before_model_extraction(
|
||||||
tmp_path: Path, monkeypatch, capsys
|
tmp_path: Path, monkeypatch, capsys
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ def test_behavior_manifest_is_deterministic_and_never_emits_config_secrets(tmp_p
|
||||||
memory:
|
memory:
|
||||||
enabled: true
|
enabled: true
|
||||||
search: sqlite_fts5
|
search: sqlite_fts5
|
||||||
|
nudge_interval: 0
|
||||||
|
skills:
|
||||||
|
creation_nudge_interval: 0
|
||||||
gateway:
|
gateway:
|
||||||
telegram:
|
telegram:
|
||||||
bot_token: super-secret-token
|
bot_token: super-secret-token
|
||||||
|
|
@ -52,6 +55,8 @@ gateway:
|
||||||
|
|
||||||
assert first["behavior_sha256"] == second["behavior_sha256"]
|
assert first["behavior_sha256"] == second["behavior_sha256"]
|
||||||
assert first["model_runtime"]["default_model"] == "anthropic/claude-sonnet-4-6"
|
assert first["model_runtime"]["default_model"] == "anthropic/claude-sonnet-4-6"
|
||||||
|
assert first["model_runtime"]["automatic_memory_review_interval"] == 0
|
||||||
|
assert first["model_runtime"]["automatic_skill_review_interval"] == 0
|
||||||
assert first["teleo_infrastructure_runtime"]["source_tree"]["file_count"] == 2
|
assert first["teleo_infrastructure_runtime"]["source_tree"]["file_count"] == 2
|
||||||
assert first["components"]["persistent_memory"]["content"]["file_count"] == 1
|
assert first["components"]["persistent_memory"]["content"]["file_count"] == 1
|
||||||
encoded = json.dumps(first)
|
encoded = json.dumps(first)
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,10 @@ def test_deploy_scripts_sync_leoclean_skills_and_restart_gateway_for_runtime_cha
|
||||||
assert "hermes-agent/leoclean-skills/vps/" in auto_deploy
|
assert "hermes-agent/leoclean-skills/vps/" in auto_deploy
|
||||||
assert "hermes-agent/leoclean-plugins/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 'HERMES_PATCH_DIR="/home/teleo/.hermes/teleo-runtime-patches"' in auto_deploy
|
||||||
|
assert 'LEOCLEAN_CONFIG="/home/teleo/.hermes/profiles/leoclean/config.yaml"' in auto_deploy
|
||||||
|
assert 'LEOCLEAN_POLICY_PATCH="$HERMES_PATCH_DIR/enforce_leoclean_learning_policy.py"' in auto_deploy
|
||||||
|
assert 'python3 "$LEOCLEAN_POLICY_PATCH" "$LEOCLEAN_CONFIG"' in auto_deploy
|
||||||
|
assert "Leoclean learning-policy drift repaired" in auto_deploy
|
||||||
assert '"status": "installed_now"' in auto_deploy
|
assert '"status": "installed_now"' in auto_deploy
|
||||||
assert "Hermes response-transform drift repaired" in auto_deploy
|
assert "Hermes response-transform drift repaired" in auto_deploy
|
||||||
assert "deploy/leoclean-gateway-restart-required.sh" in auto_deploy
|
assert "deploy/leoclean-gateway-restart-required.sh" in auto_deploy
|
||||||
|
|
@ -145,6 +149,8 @@ def test_deploy_scripts_sync_leoclean_skills_and_restart_gateway_for_runtime_cha
|
||||||
assert "hermes-agent/leoclean-skills/vps/" in manual_deploy
|
assert "hermes-agent/leoclean-skills/vps/" in manual_deploy
|
||||||
assert "hermes-agent/leoclean-plugins/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 'VPS_HERMES_PATCHES="/home/teleo/.hermes/teleo-runtime-patches"' in manual_deploy
|
||||||
|
assert 'VPS_LEOCLEAN_POLICY_PATCH="$VPS_HERMES_PATCHES/enforce_leoclean_learning_policy.py"' in manual_deploy
|
||||||
|
assert "Leoclean gateway restarted." in manual_deploy
|
||||||
assert "systemctl is-active --quiet leoclean-gateway.service" in manual_deploy
|
assert "systemctl is-active --quiet leoclean-gateway.service" in manual_deploy
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue