"""Harness app wrapper for per-user research bucket instructions.""" from __future__ import annotations import asyncio from contextlib import asynccontextmanager from dataclasses import replace from typing import TYPE_CHECKING, Any from fast_agent import AgentRequest, AppOpenRequest, HarnessAppContext from mcp.types import TextContent try: from .app_jobs import current_research_job from .app_observability import capture_markdown_report from .archive_provisioning import ensure_archive_space from .research_workspace import ( ResearchWorkspace, current_research_workspace, ensure_workspace, ) except ImportError: # loaded as top-level module from the fast-agent home from research.app_jobs import current_research_job from research.app_observability import capture_markdown_report from research.archive_provisioning import ensure_archive_space from research.research_workspace import ( ResearchWorkspace, current_research_workspace, ensure_workspace, ) if TYPE_CHECKING: from collections.abc import AsyncIterator, Mapping class ResearchHarnessApp: """Intercept opened harness sessions and wrap invocations.""" def __init__(self, context: HarnessAppContext) -> None: self._default_app = context.default_app @asynccontextmanager async def open(self, request: AppOpenRequest | None = None) -> AsyncIterator[Any]: resolved = request or AppOpenRequest() async with self._default_app.open(resolved) as session: yield ResearchHarnessSession(session, resolved.metadata) class ResearchHarnessSession: """Per-open-session wrapper that injects bucket instructions per request.""" def __init__(self, session: Any, open_metadata: Mapping[str, object]) -> None: self._session = session self._open_metadata = dict(open_metadata) @property def agent_app(self) -> Any: return self._session.agent_app @property def env(self) -> Any: return self._session.env async def invoke(self, request: AgentRequest) -> Any: workspace = await asyncio.to_thread( ensure_workspace, auth=request.auth, request_metadata={ **request.metadata, "request_session_id": request.session_id, }, open_metadata=self._open_metadata, ) workspace = await self._with_archive_space(workspace) forwarded = self._with_bucket_instructions(request, workspace) workspace_token = current_research_workspace.set(workspace) try: if workspace.bearer_token is None: response = await self._session.invoke(forwarded) else: from fast_agent.mcp.auth.context import request_bearer_token auth_token = request_bearer_token.set(workspace.bearer_token) try: response = await self._session.invoke(forwarded) finally: request_bearer_token.reset(auth_token) if job := current_research_job.get(): await capture_markdown_report(job) return response finally: current_research_workspace.reset(workspace_token) async def _with_archive_space( self, workspace: ResearchWorkspace, ) -> ResearchWorkspace: try: archive = await asyncio.to_thread( ensure_archive_space, username=workspace.username, bucket_id=workspace.bucket_id, token=workspace.bearer_token, ) except Exception as exc: return replace( workspace, archive_status="error", archive_error=f"{type(exc).__name__}: {exc}", ) return replace( workspace, archive_space_id=archive.space_id, archive_space_url=archive.space_url, archive_app_url=archive.app_url, archive_status=archive.status, archive_template_version=archive.template_version, archive_installed_version=archive.installed_version, ) def _with_bucket_instructions( self, request: AgentRequest, workspace: ResearchWorkspace ) -> AgentRequest: instructions = "\n".join( [ "Verified research workspace for this request:", f"- Hugging Face user: `{workspace.username}`", f"- Bucket: `{workspace.bucket_id}`", f"- Root: `{workspace.root}`", f"- Scratch/workings: `{workspace.scratch}`", f"- Final user-facing outputs: `{workspace.output}`", *( [ f"- Report archive Space: `{workspace.archive_space_id}`", f"- Report archive: {workspace.archive_space_url}", f"- Archive app: {workspace.archive_app_url}", f"- Archive status: `{workspace.archive_status}`", ] if workspace.archive_space_id else [] ), "The workspace was verified before this prompt was sent.", f"Hugging Face MCP authentication is verified for `{workspace.username}`.", "The same caller bearer token is forwarded to Hugging Face MCP tool calls.", "If authentication status must be reported, call `hf__hf_whoami`; do not infer it from cached server instructions.", "Write the final Markdown report to the bucket-relative path `output/report.md` unless the user requests another filename.", "That path is inside the verified Hugging Face bucket session, not the server's local filesystem.", "Use Hugging Face filesystem tools for bucket files. Never create `output/`, `scratch/`, or report artifacts in the local working directory.", "When you report a Hugging Face bucket artifact to the user, include both the `hf://` path and the accessible HTTPS URL.", "Convert `hf://buckets///` to `https://huggingface.co/buckets///tree/`.", f"Default report URL: `https://huggingface.co/buckets/{workspace.bucket_id}/tree/{workspace.session_id}/output/report.md`", ] ) return replace( request, message=_prepend_text(request.message, instructions), metadata={ **request.metadata, "research_username": workspace.username, "research_session_id": workspace.session_id, "research_bucket_id": workspace.bucket_id, "research_bucket_root": workspace.root, "research_scratch": workspace.scratch, "research_output": workspace.output, "research_marker_paths": list(workspace.marker_paths), "research_archive_space_id": workspace.archive_space_id, "research_archive_space_url": workspace.archive_space_url, "research_archive_app_url": workspace.archive_app_url, "research_archive_status": workspace.archive_status, "research_archive_template_version": ( workspace.archive_template_version ), "research_archive_installed_version": ( workspace.archive_installed_version ), "research_archive_error": workspace.archive_error, }, ) def create_app(context: HarnessAppContext) -> ResearchHarnessApp: return ResearchHarnessApp(context) def _prepend_text(message: Any, text: str) -> Any: content = list(message.content) content.insert( 0, TextContent( type="text", text=f"{text}\n\nUser request follows.", ), ) return message.model_copy(update={"content": content})