diff --git a/setup-hermes-fork.sh b/setup-hermes-fork.sh deleted file mode 100644 index 096274c..0000000 --- a/setup-hermes-fork.sh +++ /dev/null @@ -1,278 +0,0 @@ -#!/usr/bin/env bash -# setup-hermes-fork.sh -# Rode este script DENTRO do clone do repo hermes-agent-custom. -# Ele cria/atualiza os 4 arquivos do patch e não altera mais nada. -# -# Uso: -# cd ~/code/hermes-agent-custom -# bash setup-hermes-fork.sh -# git add . -# git commit -m "fix: skills_api as aiohttp routes injected into start()" -# git push - -set -euo pipefail - -if [ ! -f "Dockerfile" ]; then - echo "❌ Dockerfile não encontrado. Rode este script na raiz do repo hermes-agent-custom." - exit 1 -fi - -echo "📁 Criando diretório patches/..." -mkdir -p patches - -# --------------------------------------------------------------------------- -# 1) patches/skills_api.py — aiohttp RouteTableDef (NÃO é FastAPI) -# --------------------------------------------------------------------------- -# IMPORTANTE: o api_server.py do Hermes usa aiohttp.web, não FastAPI. -# Por isso expomos um aiohttp RouteTableDef e registramos via -# `app.add_routes(skills_routes)` dentro do método start() do adapter. -echo "✍️ patches/skills_api.py" -cat > patches/skills_api.py << 'PY_EOF' -""" -Skills API (aiohttp) — injetado no api_server.py do Hermes via apply_patch.py. - -Expõe: - GET /api/skills → lista skills do tenant - POST /api/skills/sync → upsert em lote (chamado pelo Lovable) - GET /api/skills/health → ping -""" -from __future__ import annotations - -import json -import os -from pathlib import Path -from typing import Any - -from aiohttp import web - -skills_routes = web.RouteTableDef() - -SKILLS_DIR = Path(os.environ.get("HERMES_SKILLS_DIR", "/opt/data/.hermes/skills")) -SYNC_TOKEN = os.environ.get("HERMES_SKILLS_SYNC_TOKEN", "") - - -def _ensure_dir() -> None: - SKILLS_DIR.mkdir(parents=True, exist_ok=True) - - -def _check_auth(token: str | None) -> None: - if not SYNC_TOKEN: - raise web.HTTPInternalServerError(reason="HERMES_SKILLS_SYNC_TOKEN não configurado") - if token != SYNC_TOKEN: - raise web.HTTPUnauthorized(reason="Token inválido") - - -@skills_routes.get("/api/skills/health") -async def health(_request: web.Request) -> web.Response: - _ensure_dir() - return web.json_response( - {"ok": True, "dir": str(SKILLS_DIR), "count": len(list(SKILLS_DIR.glob("*.md")))} - ) - - -@skills_routes.get("/api/skills") -async def list_skills(_request: web.Request) -> web.Response: - _ensure_dir() - items: list[dict[str, Any]] = [] - for f in sorted(SKILLS_DIR.glob("*.md")): - items.append({"name": f.stem, "size": f.stat().st_size}) - return web.json_response({"skills": items}) - - -@skills_routes.post("/api/skills/sync") -async def sync_skills(request: web.Request) -> web.Response: - _check_auth(request.headers.get("X-Sync-Token")) - _ensure_dir() - - try: - payload = await request.json() - except json.JSONDecodeError: - raise web.HTTPBadRequest(reason="JSON inválido") - - skills = payload.get("skills") or [] - if not isinstance(skills, list): - raise web.HTTPBadRequest(reason="skills deve ser uma lista") - - written: list[str] = [] - for s in skills: - name = (s.get("name") or "").strip() - body = s.get("markdown") or "" - if not name or "/" in name or ".." in name: - continue - target = SKILLS_DIR / f"{name}.md" - target.write_text(body, encoding="utf-8") - written.append(name) - - if payload.get("prune"): - keep = set(written) - for f in SKILLS_DIR.glob("*.md"): - if f.stem not in keep: - f.unlink(missing_ok=True) - - return web.json_response({"written": written, "count": len(written)}) -PY_EOF - -# --------------------------------------------------------------------------- -# 2) patches/apply_patch.py — injeta add_routes dentro de start() (idempotente) -# --------------------------------------------------------------------------- -# Estratégia: -# - Procura o trecho dentro de APIServerAdapter.start() onde o -# `web.Application(...)` é instanciado e atribuído (ex.: `self._app = web.Application(...)` -# ou `app = web.Application(...)`). -# - Logo após essa atribuição, injeta: -# try: -# from skills_api import skills_routes as _skills_routes -# .add_routes(_skills_routes) -# print("[hermes-custom] skills_api routes registered", flush=True) -# except Exception as _e: -# print(f"[hermes-custom] failed to register skills_api: {_e}", flush=True) -# - Idempotente: se o marcador já existir, não faz nada. -echo "✍️ patches/apply_patch.py" -cat > patches/apply_patch.py << 'PY_EOF' -""" -Injeta o registro das rotas aiohttp do skills_api dentro do método start() -do APIServerAdapter no api_server.py do Hermes. - -Idempotente: se o marcador já existir, não faz nada. -""" -from __future__ import annotations - -import re -import sys -from pathlib import Path - -MARKER = "# >>> hermes-custom: skills_api <<<" - -# Regex: captura uma linha que cria a aiohttp Application, ex: -# self._app = web.Application(...) -# app = web.Application(...) -APP_ASSIGN_RE = re.compile( - r"^(?P[ \t]+)(?P(?:self\._?app|app))\s*=\s*web\.Application\([^)]*\)\s*$", - re.MULTILINE, -) - - -def build_injection(indent: str, app_var: str) -> str: - lines = [ - "", - f"{indent}{MARKER}", - f"{indent}try:", - f"{indent} from skills_api import skills_routes as _skills_routes", - f"{indent} {app_var}.add_routes(_skills_routes)", - f'{indent} print("[hermes-custom] skills_api routes registered", flush=True)', - f"{indent}except Exception as _e:", - f'{indent} print(f"[hermes-custom] failed to register skills_api: {{_e}}", flush=True)', - f"{indent}# <<< hermes-custom: skills_api >>>", - ] - return "\n".join(lines) - - -def main(target: str) -> int: - p = Path(target) - if not p.exists(): - print(f"[apply_patch] target not found: {target}", file=sys.stderr) - return 1 - - src = p.read_text(encoding="utf-8") - if MARKER in src: - print("[apply_patch] already applied, skipping") - return 0 - - match = APP_ASSIGN_RE.search(src) - if not match: - print( - "[apply_patch] ERROR: não encontrei `... = web.Application(...)` no api_server.py", - file=sys.stderr, - ) - return 2 - - indent = match.group("indent") - app_var = match.group("lhs") - insert_at = match.end() - injection = build_injection(indent, app_var) - - new_src = src[:insert_at] + injection + src[insert_at:] - p.write_text(new_src, encoding="utf-8") - print( - f"[apply_patch] injected after `{app_var} = web.Application(...)` " - f"at offset {insert_at} (indent={len(indent)} spaces)" - ) - return 0 - - -if __name__ == "__main__": - target = sys.argv[1] if len(sys.argv) > 1 else "/opt/hermes/gateway/platforms/api_server.py" - sys.exit(main(target)) -PY_EOF - -# --------------------------------------------------------------------------- -# 3) entrypoint.sh — adiciona criação do diretório de skills -# --------------------------------------------------------------------------- -echo "✍️ entrypoint.sh" -cat > entrypoint.sh << 'SH_EOF' -#!/bin/bash -set -e - -# SOUL.md override via env -if [ -n "$HERMES_SOUL_OVERRIDE" ]; then - echo "$HERMES_SOUL_OVERRIDE" > /opt/data/.hermes/SOUL.md -fi - -# Garante diretório de skills -mkdir -p /opt/data/.hermes/skills - -exec /opt/hermes/docker/entrypoint.sh "$@" -SH_EOF -chmod +x entrypoint.sh - -# --------------------------------------------------------------------------- -# 4) Dockerfile — copia patches, expõe skills_api no PYTHONPATH e aplica o patch -# --------------------------------------------------------------------------- -echo "✍️ Dockerfile" -cat > Dockerfile << 'DOCKER_EOF' -FROM nousresearch/hermes-agent:latest - -USER root - -RUN mkdir -p /opt/data/.hermes /opt/data/.hermes/skills /opt/hermes-custom - -# Config + SOUL default -RUN printf 'model:\n provider: ollama-cloud\n default: gemma4:31b-cloud\n' > /opt/data/.hermes/config.yaml -RUN printf 'Você é Mika, uma assistente pessoal de IA criada pela DomCo.' > /opt/data/.hermes/SOUL.md - -# Patches -COPY patches/skills_api.py /opt/hermes-custom/skills_api.py -COPY patches/apply_patch.py /opt/hermes-custom/apply_patch.py - -# Disponibiliza skills_api no PYTHONPATH e injeta add_routes dentro de start() -ENV PYTHONPATH="/opt/hermes-custom:${PYTHONPATH}" -RUN python3 /opt/hermes-custom/apply_patch.py /opt/hermes/gateway/platforms/api_server.py - -# Entrypoint custom -COPY entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] -CMD ["gateway"] -DOCKER_EOF - -echo "" -echo "✅ Tudo pronto. Arquivos criados/atualizados:" -echo " - Dockerfile (sobrescrito)" -echo " - entrypoint.sh (sobrescrito, +x)" -echo " - patches/skills_api.py (sobrescrito — agora aiohttp)" -echo " - patches/apply_patch.py (sobrescrito — injeta dentro de start())" -echo "" -echo "⚠️ IMPORTANTE: o patch anterior foi anexado ao FIM do api_server.py e" -echo " quebrou o build (name 'app' is not defined). O novo apply_patch.py" -echo " é idempotente pelo marcador, MAS o marcador antigo ainda está no" -echo " arquivo da imagem base? NÃO — o build sempre parte da imagem" -echo " nousresearch/hermes-agent:latest limpa, então não há resíduo." -echo "" -echo "Próximos passos:" -echo " git add Dockerfile entrypoint.sh patches/" -echo " git commit -m 'fix: skills_api as aiohttp routes injected into start()'" -echo " git push" -echo "" -echo "Depois do deploy, teste:" -echo " curl https://.up.railway.app/api/skills/health"