#!/usr/bin/env python3 """Add explicit parent lineage to Hermes API-created sessions.""" from __future__ import annotations import argparse from pathlib import Path BEFORE = ''' model = body.get("model") or self._model_name system_prompt = body.get("system_prompt") if system_prompt is not None and not isinstance(system_prompt, str): return web.json_response(_openai_error("system_prompt must be a string", code="invalid_system_prompt"), status=400) db.create_session(session_id, "api_server", model=str(model) if model else None, system_prompt=system_prompt) ''' AFTER = ''' model = body.get("model") or self._model_name system_prompt = body.get("system_prompt") if system_prompt is not None and not isinstance(system_prompt, str): return web.json_response(_openai_error("system_prompt must be a string", code="invalid_system_prompt"), status=400) # API workers are first-class children of the objective that launched # them. Accept a JSON field for normal clients and a header for thin # relays that cannot extend their request schema. metadata = body.get("metadata") metadata_parent = metadata.get("parent_session_id") if isinstance(metadata, dict) else None raw_parent = body.get("parent_session_id") or metadata_parent or request.headers.get( "X-Hermes-Parent-Session-Id" ) parent_session_id = str(raw_parent).strip() if raw_parent else None if parent_session_id: if ( len(parent_session_id) > self._MAX_SESSION_HEADER_LEN or re.search(r'[\\r\\n\\x00]', parent_session_id) or _is_path_unsafe(parent_session_id) or parent_session_id == session_id ): return web.json_response(_openai_error("Invalid parent session ID", code="invalid_parent_session_id"), status=400) if not db.get_session(parent_session_id): return web.json_response(_openai_error(f"Parent session not found: {parent_session_id}", code="parent_session_not_found"), status=404) db.create_session( session_id, "api_server", model=str(model) if model else None, system_prompt=system_prompt, parent_session_id=parent_session_id, ) ''' RUNS_BEFORE = ''' run_id = f"run_{uuid.uuid4().hex}" session_id = body.get("session_id") or stored_session_id or run_id # Approval queues gate host-side tool execution and must be isolated ''' RUNS_AFTER = ''' run_id = f"run_{uuid.uuid4().hex}" session_id = body.get("session_id") or stored_session_id or run_id # Persist API-run lineage before the agent starts. Automated callers # may omit a parent, so a deployment can provide a narrowly matched # default without grouping ordinary interactive conversations. metadata = body.get("metadata") metadata_parent = metadata.get("parent_session_id") if isinstance(metadata, dict) else None raw_parent = body.get("parent_session_id") or metadata_parent or request.headers.get( "X-Hermes-Parent-Session-Id" ) default_parent = os.environ.get("HERMES_API_DEFAULT_PARENT_SESSION_ID", "").strip() default_prefixes = tuple( item.strip() for item in os.environ.get("HERMES_API_DEFAULT_PARENT_MATCH_PREFIXES", "").split("||") if item.strip() ) if not raw_parent and default_parent and default_prefixes and user_message.startswith(default_prefixes): raw_parent = default_parent parent_session_id = str(raw_parent).strip() if raw_parent else None from gateway.session import _is_path_unsafe if parent_session_id: if ( len(parent_session_id) > self._MAX_SESSION_HEADER_LEN or re.search(r'[\\r\\n\\x00]', parent_session_id) or _is_path_unsafe(parent_session_id) or parent_session_id == session_id ): return web.json_response(_openai_error("Invalid parent session ID", code="invalid_parent_session_id"), status=400) db = self._ensure_session_db() if db is None: return web.json_response(_openai_error("Session database unavailable", code="session_db_unavailable"), status=503) if not db.get_session(parent_session_id): return web.json_response(_openai_error(f"Parent session not found: {parent_session_id}", code="parent_session_not_found"), status=404) if not db.get_session(session_id): db.create_session( session_id, "api_server", model=str(body.get("model") or self._model_name or ""), system_prompt=instructions if isinstance(instructions, str) else None, parent_session_id=parent_session_id, ) incident = re.search(r"(?:for|Analyze) incident ([^\\s.]+)(?:\\.|\\s)", user_message) if incident and parent_session_id == default_parent: parts = incident.group(1).split("/") label = " · ".join(parts[1:3]) if len(parts) >= 3 else incident.group(1) db.set_session_title(session_id, f"Sonar · {label} · {session_id[-8:]}") # Approval queues gate host-side tool execution and must be isolated ''' def patch(source: Path, destination: Path) -> None: """Apply the narrow session-lineage extension and fail on upstream drift.""" content = source.read_text(encoding="utf-8") if BEFORE not in content: raise RuntimeError("Hermes API session patch context changed") if RUNS_BEFORE not in content: raise RuntimeError("Hermes API runs patch context changed") destination.parent.mkdir(parents=True, exist_ok=True) content = content.replace(BEFORE, AFTER, 1) destination.write_text(content.replace(RUNS_BEFORE, RUNS_AFTER, 1), encoding="utf-8") def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("source", type=Path) parser.add_argument("destination", type=Path) args = parser.parse_args() patch(args.source, args.destination) return 0 if __name__ == "__main__": raise SystemExit(main())