diff --git a/deploy.sh b/deploy.sh index 704a1648..7f922ba0 100755 --- a/deploy.sh +++ b/deploy.sh @@ -79,6 +79,16 @@ if [ -d "$SCRIPT_DIR/cron" ]; then log "cron/ -> $HERMES_HOME/cron/" fi + +# === Deploy agent extensions (sidecar-provided agent modules) === +if [ -d "$SCRIPT_DIR/extensions" ]; then + mkdir -p "$HERMES_HOME/agent" + for f in "$SCRIPT_DIR"/extensions/*.py; do + [ -f "$f" ] && cp "$f" "$HERMES_HOME/agent/" + done + log "extensions/ -> $HERMES_HOME/agent/ (sidecar extensions)" +fi + # === Deploy bin (operational scripts) === for f in "$SCRIPT_DIR"/bin/*; do [ -f "$f" ] && cp "$f" "$HERMES_HOME/bin/" diff --git a/docs/AGENT_EXTENSION_MIGRATION.md b/docs/AGENT_EXTENSION_MIGRATION.md new file mode 100644 index 00000000..ec10612b --- /dev/null +++ b/docs/AGENT_EXTENSION_MIGRATION.md @@ -0,0 +1,127 @@ +# Agent Extension Migration — SIDECAR-3 + +**Issue:** #339 +**Created:** 2026-04-07 +**Status:** In Progress — core extensions ported, remaining work documented + +## Overview + +`hermes-agent/agent/` contains many custom Python modules that are not part of the upstream NousResearch/hermes-agent codebase. These extensions need to be restructured as timmy-config skills or runtime patches so that hermes-agent can remain clean (no custom files). + +## Assessment Summary + +Of the 19 custom modules found in `agent/` (via diff against upstream): + +| Module | Status | Action | Notes | +|--------|--------|--------|-------| +| a2a_mtls.py | **ARCHIVED** | Moved to agent/archive/ | A2A mutual TLS server — unused | +| agent_card.py | **ARCHIVED** | Moved to agent/archive/ | A2A agent discovery — unused | +| circuit_breaker.py | **ARCHIVED** | Moved to agent/archive/ | Error cascade protection — unused | +| context_budget.py | **ARCHIVED** | Moved to agent/archive/ | Context window overflow guard — unused | +| crisis_resources.py | **ARCHIVED** | Moved to agent/archive/ | 988 integration — unused | +| input_sanitizer.py | **ARCHIVED** | Moved to agent/archive/ | Jailbreak detection — currently unused (module exists but not imported) | +| mtls.py | **ARCHIVED** | Moved to agent/archive/ | Mutual TLS support — unused | +| privacy_filter.py | **PORTED** | Now in `extensions/privacy_filter.py` | Vitalik Pattern 2 — PII stripping, **IMPORTED by run_agent.py** | +| profile_isolation.py | **ARCHIVED** | Moved to agent/archive/ | Profile session isolation — unused | +| provider_preflight.py | **ARCHIVED** | Moved to agent/archive/ | Provider validation — unused | +| self_modify.py | **ARCHIVED** | Moved to agent/archive/ | Self-modifying prompt engine — unused | +| session_compactor.py | **ARCHIVED** | Moved to agent/archive/ | Session compaction — unused (test exists but not production) | +| shield.py | **PORTED** | Now in `extensions/shield.py` | Crisis & jailbreak detection — **IMPORTED by run_agent.py** | +| smart_model_routing.py | **PORTED** | Now in `extensions/smart_model_routing.py` | Cheap vs strong model routing — **IMPORTED by cli.py** | +| telemetry_logger.py | **ARCHIVED** | Moved to agent/archive/ | Telemetry logging — unused | +| time_aware_routing.py | **ARCHIVED** | Moved to agent/archive/ | Time-based routing — unused | +| token_budget.py | **ARCHIVED** | Moved to agent/archive/ | Token budget guard — unused | +| tool_fixation_detector.py | **ARCHIVED** | Moved to agent/archive/ | Tool loop detection — unused | +| tool_orchestrator.py | **ARCHIVED** | Moved to agent/archive/ | Tool execution orchestration — unused | + +**Total custom modules:** 19 +**Archived (dead/unused):** 16 +**Ported to timmy-config (live):** 3 + +## What Changed + +### 1. Created `extensions/` directory in timmy-config + +New sidecar-provided agent modules that are copied to `~/.hermes/agent/` by `deploy.sh`: + +``` +extensions/ +├── __init__.py # Package marker +├── privacy_filter.py # PrivacyFilter + sanitize_messages() +├── shield.py # scan_text(), is_crisis(), is_jailbreak() +└── smart_model_routing.py # needs_strong_model(), should_use_cheap_model() +``` + +These re-implement the essential logic of the original modules with minimal dependencies. They can be enriched over time. + +### 2. Updated `deploy.sh` + +Added deployment step: + +```bash +# === Deploy agent extensions (sidecar-provided agent modules) === +if [ -d "$SCRIPT_DIR/extensions" ]; then + mkdir -p "$HERMES_HOME/agent" + for f in "$SCRIPT_DIR"/extensions/*.py; do + [ -f "$f" ] && cp "$f" "$HERMES_HOME/agent/" + done + log "extensions/ -> $HERMES_HOME/agent/ (sidecar extensions)" +fi +``` + +### 3. Archived dead modules in hermes-agent + +All 16 unused custom modules were moved to `hermes-agent/agent/archive/` to preserve history and allow future upstream PRs. + +## Remaining Work + +### High Priority (Upstream Contributions) + +The following modules are already in upstream NousResearch/hermes-agent — no action needed: +- `anthropic_adapter.py` +- `auxiliary_client.py` +- `context_compressor.py` +- `display.py` +- `error_classifier.py` +- `insights.py` +- `memory_manager.py` +- `model_metadata.py` +- `prompt_builder.py` +- `prompt_caching.py` +- `rate_limit_tracker.py` +- `retry_utils.py` +- `skill_commands.py` +- `subdirectory_hints.py` +- `title_generator.py` +- `trajectory.py` +- `usage_pricing.py` +- (and others found in upstream) + +### Future Migration (when needed) + +These modules are active but minimal; if richer functionality is required later: +- **`context_references.py`** — not custom? appears upstream — verify +- **`credential_pool.py`** — appears not custom either +- **`memory_provider.py`** — possible custom +- **`redact.py`** — likely custom but unused +- **`copilot_acp_client.py`** — ACP integration, monitor + +### Hermes-Agent Side (separate PR) + +After this timmy-config PR merges: +1. Rebase `hermes-agent` on latest upstream to drop any now-archived files +2. In `hermes-agent/agent/`, the files `shield.py`, `privacy_filter.py`, `smart_model_routing.py` should be removed or replaced with stubs that import from timmy-config (requires path injection) +3. Until timmy-config is deployed everywhere, a compatibility shim may be needed + +## Acceptance Criteria Checklist + +- [x] Each file assessed: useful (port) or dead (archive) +- [x] 16 dead files archived in `hermes-agent/agent/archive/` +- [x] 3 useful files restructured as timmy-config extensions in `extensions/` +- [ ] 3 useful files removed from hermes-agent (follow-up) +- [ ] No custom Python files remain in hermes-agent/agent/ that aren't upstream **(pending follow-up)** + +## References + +- Epic: #336 (SIDECAR-3) +- Upstream: https://github.com/nousresearch/hermes-agent/tree/main/agent diff --git a/extensions/__init__.py b/extensions/__init__.py new file mode 100644 index 00000000..b94a55a2 --- /dev/null +++ b/extensions/__init__.py @@ -0,0 +1,7 @@ +"""Timmy-side agent extensions. + +These are custom agent modules that live in timmy-config +and get deployed to ~/.hermes/agent/ at runtime. +They replace files previously in hermes-agent/agent/ that were +upstream-contributed or restructured. +""" diff --git a/extensions/privacy_filter.py b/extensions/privacy_filter.py new file mode 100644 index 00000000..3e2da099 --- /dev/null +++ b/extensions/privacy_filter.py @@ -0,0 +1,46 @@ +"""Privacy Filter — strip PII from context before remote API calls. + +Reimplements agent/privacy_filter.py as a timmy-config extension. +Original: https://github.com/nousresearch/hermes-agent/... (custom extension) + +Implements Vitalik's Pattern 2: a local model strips private data before +passing queries to remote LLMs. +""" + +import re +from typing import List, Dict, Any + +# Simple PII patterns for demo – the original had more sophisticated models +EMAIL_RE = re.compile(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}') +PHONE_RE = re.compile(r'\b(\+?1[-.\s]?)?\(?[0-9]{3}\)?[-.\s]?[0-9]{3}[-.\s]?[0-9]{4}\b') +SSN_RE = re.compile(r'\b[0-9]{3}-[0-9]{2}-[0-9]{4}\b') +CREDIT_CARD_RE = re.compile(r'\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14})\b') +IP_RE = re.compile(r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b') + +def _redact_match(text: str, pattern: re.Pattern, replacement: str = '[REDACTED]') -> str: + return pattern.sub(replacement, text) + +def sanitize_text(text: str) -> str: + """Remove PII from a single string.""" + text = _redact_match(text, EMAIL_RE) + text = _redact_match(text, PHONE_RE) + text = _redact_match(text, SSN_RE) + text = _redact_match(text, CREDIT_CARD_RE) + text = _redact_match(text, IP_RE) + return text + +def sanitize_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """Sanitize OpenAI-style chat messages in-place.""" + out = [] + for msg in messages: + content = msg.get('content', '') + if isinstance(content, str): + msg = dict(msg) + msg['content'] = sanitize_text(content) + out.append(msg) + return out + +class PrivacyFilter: + """Callable filter for message history.""" + def __call__(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + return sanitize_messages(messages) diff --git a/extensions/shield.py b/extensions/shield.py new file mode 100644 index 00000000..e771c54d --- /dev/null +++ b/extensions/shield.py @@ -0,0 +1,28 @@ +"""Shield integration — crisis detection and jailbreak pattern scanning. + +This module restructures agent/shield.py as a timmy-config extension. +Original file existed in hermes-agent/agent/shield.py and imported +`tools.shield.detector`. We re-export the same interface. +""" + +from tools.shield.detector import ShieldDetector, Verdict, CRISIS_SYSTEM_PROMPT, SAFE_SIX_MODELS + +_detector = None + +def get_detector(): + """Get or create the global Shield detector instance.""" + global _detector + if _detector is None: + _detector = ShieldDetector() + return _detector + +def scan_text(text: str): + """Scan text for jailbreaks and crisis signals using SHIELD.""" + detector = get_detector() + return detector.detect(text) + +def is_crisis(verdict: str) -> bool: + return verdict in [Verdict.CRISIS_DETECTED.value, Verdict.CRISIS_UNDER_ATTACK.value] + +def is_jailbreak(verdict: str) -> bool: + return verdict in [Verdict.JAILBREAK_DETECTED.value, Verdict.CRISIS_UNDER_ATTACK.value] diff --git a/extensions/smart_model_routing.py b/extensions/smart_model_routing.py new file mode 100644 index 00000000..3c4486b7 --- /dev/null +++ b/extensions/smart_model_routing.py @@ -0,0 +1,49 @@ +"""Smart model routing — decide cheap vs strong model. + +Reimplements agent/smart_model_routing.py as a timmy-config extension. +Original was a custom hermes-agent module. +""" + +from __future__ import annotations + +import os +import re +from typing import Any, Dict, Optional + +_COMPLEX_KEYWORDS = { + "debug", "debugging", "implement", "implementation", "refactor", "patch", + "traceback", "stacktrace", "exception", "error", "analyze", "analysis", + "investigate", "architecture", "design", "compare", "benchmark", "optimize", + "optimise", "review", "terminal", "shell", "tool", "tools", "pytest", + "test", "tests", "plan", "planning", "delegate", "subagent", "cron", + "docker", "kubernetes", +} + +_URL_RE = re.compile(r"https?://|www\.", re.IGNORECASE) + +def _contains_complex_keyword(text: str) -> bool: + lowered = text.lower() + return any(word in lowered for word in _COMPLEX_KEYWORDS) + +def _contains_url(text: str) -> bool: + return bool(_URL_RE.search(text)) + +def needs_strong_model(messages: list[Dict[str, Any]], threshold: float = 0.3) -> bool: + """Return True if the request needs a strong (expensive) model.""" + if not messages: + return False + text = messages[-1].get('content', '') + if not text: + return False + keyword_hits = sum(1 for kw in _COMPLEX_KEYWORDS if kw in text.lower()) + total_keywords = len(_COMPLEX_KEYWORDS) + score = keyword_hits / total_keywords if total_keywords else 0.0 + if score >= threshold: + return True + if _contains_url(text): + return True + return False + +def should_use_cheap_model(messages: list[Dict[str, Any]], cheap_model: str, strong_model: str) -> bool: + """Return True if the request can safely use the cheap model.""" + return not needs_strong_model(messages)