diff --git a/archivebox/opencode/templates/opencode/agent.html b/archivebox/opencode/templates/opencode/agent.html index 8a252295..e6f41a67 100644 --- a/archivebox/opencode/templates/opencode/agent.html +++ b/archivebox/opencode/templates/opencode/agent.html @@ -206,6 +206,78 @@ }); })(); + {% endif %} {% endblock %} diff --git a/archivebox/opencode/urls.py b/archivebox/opencode/urls.py index 1b073502..bf8fad73 100644 --- a/archivebox/opencode/urls.py +++ b/archivebox/opencode/urls.py @@ -1,10 +1,11 @@ from django.urls import path, re_path -from archivebox.opencode.views import agent_view, opencode_proxy_view +from archivebox.opencode.views import agent_health_view, agent_view, opencode_proxy_view urlpatterns = [ path("", agent_view, name="opencode-agent"), + path("opencode/_archivebox/health", agent_health_view, name="opencode-health"), re_path( r"^opencode(?:/(?P.*))?$", opencode_proxy_view, diff --git a/archivebox/opencode/views.py b/archivebox/opencode/views.py index 18779188..c689abee 100644 --- a/archivebox/opencode/views.py +++ b/archivebox/opencode/views.py @@ -26,6 +26,7 @@ from django.http import ( HttpRequest, HttpResponse, HttpResponseForbidden, + JsonResponse, StreamingHttpResponse, ) from django.shortcuts import redirect, render @@ -378,14 +379,29 @@ def _health(settings: dict) -> bool: return False +def _opencode_version(settings: dict) -> str: + try: + response = requests.get( + f"{settings['origin']}/global/health", + timeout=2, + ) + response.raise_for_status() + data = response.json() + return str(data.get("version") or "") if isinstance(data, dict) else "" + except (requests.RequestException, ValueError): + return "" + + def _ensure_opencode(settings: dict) -> tuple[bool, str]: global _PROCESS started_process: subprocess.Popen | None = None workdir = settings["workdir"].resolve() with _PROCESS_LOCK: - if (_PROCESS is not None and _PROCESS.poll() is None) or _health(settings): + if _health(settings): return True, "" + if _PROCESS is not None and _PROCESS.poll() is None: + _stop_owned_process(_PROCESS) try: binary, git_binary, binary_env = _resolve_binary( @@ -494,10 +510,15 @@ def agent_view(request: HttpRequest): settings["archivebox_api_url"] = api_url ok, error = _ensure_opencode(settings) recent_session_id = "" + opencode_version = "" if ok: try: with _SESSION_LOCK: recent_session_id = _ensure_default_session(settings) + opencode_version = _opencode_version(settings) + if not opencode_version: + ok = False + error = "OpenCode health check did not report its version." except (requests.RequestException, RuntimeError, ValueError) as err: ok = False error = f"OpenCode project initialization failed: {err}" @@ -514,6 +535,8 @@ def agent_view(request: HttpRequest): # open the durable session URL so a fresh browser does not land on the # transient new-session route and appear to have lost prior sessions. "proxy_url": _project_route(settings["workdir"], recent_session_id), + "proxy_prefix": _PROXY_PREFIX, + "opencode_version": opencode_version, "workdir": str(settings["workdir"].resolve()), "recent_session_id": recent_session_id, } @@ -525,6 +548,23 @@ def agent_view(request: HttpRequest): ) +def agent_health_view(request: HttpRequest): + config = _machine_config() + _require_enabled(config) + auth_response = _require_superuser(request) + if auth_response: + return auth_response + + version = _opencode_version(_settings(config)) + healthy = bool(version) + response = JsonResponse( + {"healthy": healthy, "version": version}, + status=200 if healthy else 503, + ) + response.headers["Cache-Control"] = "no-store" + return response + + def _proxy_url(settings: dict, path: str | None) -> str: rel = "/" if not path else f"/{path}" return urljoin(settings["origin"], rel) diff --git a/archivebox/tests/test_opencode_agent.py b/archivebox/tests/test_opencode_agent.py index b0ee9d2d..4bd89e2f 100644 --- a/archivebox/tests/test_opencode_agent.py +++ b/archivebox/tests/test_opencode_agent.py @@ -186,6 +186,15 @@ def test_opencode_agent_superuser_gets_admin_wrapper(admin_client, live_opencode assert f'