Compare commits
4 Commits
dispatch/3
...
fix/456-cl
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ac8d0268f | |||
| 2e59f8540d | |||
| 954fd992eb | |||
|
|
f35f56e397 |
@@ -12,6 +12,7 @@ import asyncio
|
||||
import concurrent.futures
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -544,6 +545,55 @@ def _run_job_script(script_path: str) -> tuple[bool, str]:
|
||||
except Exception as exc:
|
||||
return False, f"Script execution failed: {exc}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cloud-context warning for local-service references (#378, #456)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_LOCAL_SERVICE_PATTERNS = [
|
||||
re.compile(r'localhost:\d+', re.IGNORECASE),
|
||||
re.compile(r'127\.0\.0\.1:\d+'),
|
||||
re.compile(r'check\s+ollama', re.IGNORECASE),
|
||||
re.compile(r'ollama\s+(is\s+)?respond', re.IGNORECASE),
|
||||
re.compile(r'curl\s+localhost', re.IGNORECASE),
|
||||
re.compile(r'curl\s+127\.', re.IGNORECASE),
|
||||
re.compile(r'curl\s+local', re.IGNORECASE),
|
||||
re.compile(r'ping\s+localhost', re.IGNORECASE),
|
||||
re.compile(r'poll(ing)?\s+local', re.IGNORECASE),
|
||||
re.compile(r'check\s+service\s+respond', re.IGNORECASE),
|
||||
re.compile(r'11434'), # Ollama default port
|
||||
re.compile(r'11435'), # common alt Ollama port
|
||||
]
|
||||
|
||||
|
||||
def _detect_local_service_refs(prompt: str) -> list[str]:
|
||||
"""Return list of local-service reference descriptions found in prompt."""
|
||||
refs = []
|
||||
for pat in _LOCAL_SERVICE_PATTERNS:
|
||||
m = pat.search(prompt)
|
||||
if m:
|
||||
refs.append(m.group(0))
|
||||
return refs
|
||||
|
||||
|
||||
def _inject_cloud_context(prompt: str, refs: list[str], provider: str) -> str:
|
||||
"""Prepend a SYSTEM NOTE so the agent knows it cannot reach localhost."""
|
||||
refs_str = ", ".join(f'"{r}"' for r in refs)
|
||||
warning = (
|
||||
"[SYSTEM NOTE — cloud endpoint]
|
||||
"
|
||||
f"You are running on a cloud inference endpoint ({provider}). "
|
||||
f"Your prompt references local services: {refs_str}. "
|
||||
"You CANNOT reach localhost or any local network address from this endpoint. "
|
||||
"Do NOT attempt curl, ping, SSH, or any network calls to localhost. "
|
||||
"Instead, report to the user that this job requires a local model endpoint "
|
||||
"to check local services, and suggest they re-run with a local provider.
|
||||
|
||||
"
|
||||
)
|
||||
return warning + prompt
|
||||
|
||||
|
||||
|
||||
|
||||
def _build_job_prompt(job: dict) -> str:
|
||||
"""Build the effective prompt for a cron job, optionally loading one or more skills first."""
|
||||
@@ -817,6 +867,18 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:
|
||||
job_name,
|
||||
)
|
||||
|
||||
# Inject cloud-context warning when prompt references local services (#378)
|
||||
if _is_cloud:
|
||||
_local_refs = _detect_local_service_refs(prompt)
|
||||
if _local_refs:
|
||||
_provider_name = turn_route["runtime"].get("provider", "cloud")
|
||||
prompt = _inject_cloud_context(prompt, _local_refs, _provider_name)
|
||||
logger.info(
|
||||
"Job '%s': injected cloud-context warning for local refs: %s",
|
||||
job_name,
|
||||
_local_refs,
|
||||
)
|
||||
|
||||
_agent_kwargs = _safe_agent_kwargs({
|
||||
"model": turn_route["model"],
|
||||
"api_key": turn_route["runtime"].get("api_key"),
|
||||
|
||||
28
run_agent.py
28
run_agent.py
@@ -1001,30 +1001,10 @@ class AIAgent:
|
||||
self._session_db = session_db
|
||||
self._parent_session_id = parent_session_id
|
||||
self._last_flushed_db_idx = 0 # tracks DB-write cursor to prevent duplicate writes
|
||||
if self._session_db:
|
||||
try:
|
||||
self._session_db.create_session(
|
||||
session_id=self.session_id,
|
||||
source=self.platform or os.environ.get("HERMES_SESSION_SOURCE", "cli"),
|
||||
model=self.model,
|
||||
model_config={
|
||||
"max_iterations": self.max_iterations,
|
||||
"reasoning_config": reasoning_config,
|
||||
"max_tokens": max_tokens,
|
||||
},
|
||||
user_id=None,
|
||||
parent_session_id=self._parent_session_id,
|
||||
)
|
||||
except Exception as e:
|
||||
# Transient SQLite lock contention (e.g. CLI and gateway writing
|
||||
# concurrently) must NOT permanently disable session_search for
|
||||
# this agent. Keep _session_db alive — subsequent message
|
||||
# flushes and session_search calls will still work once the
|
||||
# lock clears. The session row may be missing from the index
|
||||
# for this run, but that is recoverable (flushes upsert rows).
|
||||
logger.warning(
|
||||
"Session DB create_session failed (session_search still available): %s", e
|
||||
)
|
||||
# Lazy session creation: defer until first message flush (#314).
|
||||
# _flush_messages_to_session_db() calls ensure_session() which uses
|
||||
# INSERT OR IGNORE — creating the row only when messages arrive.
|
||||
# This eliminates 32% of sessions that are created but never used.
|
||||
|
||||
# In-memory todo list for task planning (one per agent/session)
|
||||
from tools.todo_tool import TodoStore
|
||||
|
||||
91
tests/test_cron_cloud_context.py
Normal file
91
tests/test_cron_cloud_context.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""Tests for cloud-context warning injection (#378, #456)."""
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from cron.scheduler import (
|
||||
_LOCAL_SERVICE_PATTERNS,
|
||||
_detect_local_service_refs,
|
||||
_inject_cloud_context,
|
||||
)
|
||||
|
||||
|
||||
class TestDetectLocalServiceRefs:
|
||||
"""Pattern detection for local service references in prompts."""
|
||||
|
||||
def test_localhost_with_port(self):
|
||||
refs = _detect_local_service_refs("Check localhost:11434 is up")
|
||||
assert len(refs) >= 1
|
||||
assert any("11434" in r for r in refs)
|
||||
|
||||
def test_127_with_port(self):
|
||||
refs = _detect_local_service_refs("curl http://127.0.0.1:8080/health")
|
||||
assert len(refs) >= 1
|
||||
|
||||
def test_check_ollama(self):
|
||||
refs = _detect_local_service_refs("Check Ollama is responding")
|
||||
assert len(refs) >= 1
|
||||
|
||||
def test_ollama_responding(self):
|
||||
refs = _detect_local_service_refs("Verify Ollama responding on this machine")
|
||||
assert len(refs) >= 1
|
||||
|
||||
def test_curl_localhost(self):
|
||||
refs = _detect_local_service_refs("curl localhost and report status")
|
||||
assert len(refs) >= 1
|
||||
|
||||
def test_ping_localhost(self):
|
||||
refs = _detect_local_service_refs("ping localhost to check connectivity")
|
||||
assert len(refs) >= 1
|
||||
|
||||
def test_no_false_positive_cloud(self):
|
||||
refs = _detect_local_service_refs("Check the weather in Paris today")
|
||||
assert len(refs) == 0
|
||||
|
||||
def test_no_false_positive_api(self):
|
||||
refs = _detect_local_service_refs("Call the OpenRouter API endpoint")
|
||||
assert len(refs) == 0
|
||||
|
||||
def test_multiple_refs(self):
|
||||
refs = _detect_local_service_refs("curl localhost:11434 then ping localhost")
|
||||
assert len(refs) >= 2
|
||||
|
||||
|
||||
class TestInjectCloudContext:
|
||||
"""Cloud-context warning injection."""
|
||||
|
||||
def test_prepends_warning(self):
|
||||
prompt = "Check Ollama is responding"
|
||||
result = _inject_cloud_context(prompt, ["Check Ollama"], "nous")
|
||||
assert result.startswith("[SYSTEM NOTE")
|
||||
assert "nous" in result
|
||||
assert prompt in result
|
||||
|
||||
def test_preserves_original_prompt(self):
|
||||
prompt = "Check Ollama at localhost:11434"
|
||||
result = _inject_cloud_context(prompt, ["localhost:11434"], "openrouter")
|
||||
assert prompt in result
|
||||
|
||||
def test_mentions_cannot_reach(self):
|
||||
prompt = "curl localhost"
|
||||
result = _inject_cloud_context(prompt, ["curl localhost"], "nous")
|
||||
assert "CANNOT reach" in result or "cannot reach" in result
|
||||
|
||||
def test_suggests_local_provider(self):
|
||||
prompt = "Check Ollama"
|
||||
result = _inject_cloud_context(prompt, ["Check Ollama"], "nous")
|
||||
assert "local" in result.lower()
|
||||
|
||||
|
||||
class TestCloudBypassLocal:
|
||||
"""Local endpoints should not trigger injection."""
|
||||
|
||||
def test_local_endpoint_skips(self):
|
||||
# The caller checks _is_cloud before calling _detect_local_service_refs
|
||||
# so this is tested at integration level. Here we verify detection
|
||||
# still finds refs (the bypass is the caller\'s responsibility).
|
||||
refs = _detect_local_service_refs("Check Ollama at localhost:11434")
|
||||
assert len(refs) > 0 # Detection works, caller decides whether to inject
|
||||
Reference in New Issue
Block a user