diff --git a/apps/agent/agent/prompt.py b/apps/agent/agent/prompt.py index 0fe9c1d6..3f14cb2d 100644 --- a/apps/agent/agent/prompt.py +++ b/apps/agent/agent/prompt.py @@ -114,7 +114,8 @@ CORE_BEHAVIOR_SECTION = """--- ### Core Behavior - **Persistence:** Keep working until the current task is completely resolved. Only terminate when you are certain the task is complete. -- **Accuracy:** Never guess or make up information. Always use tools to gather accurate data about files and codebase structure.""" +- **Accuracy:** Never guess or make up information. Always use tools to gather accurate data about files and codebase structure. +- **Autonomy:** Never ask the user for permission mid-task. Run linters, fix errors, and call `commit_and_open_pr` without waiting for confirmation.""" DEPENDENCY_SECTION = """--- @@ -200,6 +201,8 @@ When you have completed your implementation, follow these steps in order: **Commit message**: Concise, focusing on the "why" rather than the "what". If not provided, the PR title is used. +**IMPORTANT: Never ask the user for permission or confirmation before calling `commit_and_open_pr`. Do not say "if you want, I can proceed" or "shall I open the PR?". When your implementation is done and checks pass, call the tool immediately and autonomously.** + 4. **Comment on the Linear ticket** via `linear_comment` immediately after `commit_and_open_pr` succeeds. Include: - A brief summary of what was done - The PR link returned by `commit_and_open_pr` @@ -214,7 +217,7 @@ When you have completed your implementation, follow these steps in order: - ``` -Always call `commit_and_open_pr` followed by `linear_comment` once implementation is complete and code quality checks pass.""" +Always call the `commit_and_open_pr` tool followed by the `linear_comment` tool once implementation is complete and code quality checks pass.""" SYSTEM_PROMPT = ( diff --git a/apps/agent/agent/server.py b/apps/agent/agent/server.py index 78577467..5d2e96fb 100644 --- a/apps/agent/agent/server.py +++ b/apps/agent/agent/server.py @@ -25,7 +25,6 @@ from deepagents import create_deep_agent from deepagents.backends.protocol import SandboxBackendProtocol from langsmith.sandbox import SandboxClientError -from .encryption import decrypt_token from .integrations.langsmith import create_langsmith_sandbox from .middleware import ( ToolErrorMiddleware, @@ -34,6 +33,7 @@ from .middleware import ( ) from .prompt import construct_system_prompt from .tools import commit_and_open_pr, fetch_url, http_request, linear_comment +from .utils.auth import save_encrypted_token_from_email from .utils.model import make_model client = get_client() @@ -234,7 +234,6 @@ DEFAULT_RECURSION_LIMIT = 1_000 async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915 """Get or create an agent with a sandbox for the given thread.""" thread_id = config["configurable"].get("thread_id", None) - logger.info("get_agent called for thread %s", thread_id) config["recursion_limit"] = DEFAULT_RECURSION_LIMIT @@ -242,10 +241,6 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915 repo_owner = repo_config.get("owner") repo_name = repo_config.get("name") - encrypted_token = config["configurable"].get("github_token_encrypted") - if encrypted_token: - github_token = decrypt_token(encrypted_token) - if thread_id is None or not graph_loaded_for_execution(config): logger.info("No thread_id or not for execution, returning agent without sandbox") return create_deep_agent( @@ -253,6 +248,21 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915 tools=[], ).with_config(config) + # --- GitHub token resolution --- + user_email = config["configurable"].get("user_email") + source = config["configurable"].get("source") + if not source: + logger.error("Missing source for thread %s; cannot route auth failure responses", thread_id) + msg = f"GitHub auth failed for thread {thread_id}: missing source" + raise RuntimeError(msg) + try: + github_token, new_encrypted = await save_encrypted_token_from_email(user_email, source) + except ValueError as exc: + logger.error("GitHub auth failed for thread %s: %s", thread_id, str(exc)) + raise RuntimeError(str(exc)) from exc + + config["metadata"]["github_token_encrypted"] = new_encrypted + sandbox_backend = SANDBOX_BACKENDS.get(thread_id) sandbox_id = await get_sandbox_id_from_metadata(thread_id) diff --git a/apps/agent/agent/tools/commit_and_open_pr.py b/apps/agent/agent/tools/commit_and_open_pr.py index 93ea2a7a..1088d787 100644 --- a/apps/agent/agent/tools/commit_and_open_pr.py +++ b/apps/agent/agent/tools/commit_and_open_pr.py @@ -4,7 +4,6 @@ from typing import Any from langgraph.config import get_config -from ..encryption import decrypt_token from ..utils.github import ( create_github_pr, get_github_default_branch, @@ -18,6 +17,7 @@ from ..utils.github import ( git_has_unpushed_commits, git_push, ) +from ..utils.github_token import get_github_token from ..utils.sandbox_state import get_sandbox_backend_sync logger = logging.getLogger(__name__) @@ -114,6 +114,7 @@ def commit_and_open_pr( config = get_config() configurable = config.get("configurable", {}) thread_id = configurable.get("thread_id") + if not thread_id: return {"success": False, "error": "Missing thread_id in config", "pr_url": None} @@ -168,10 +169,14 @@ def commit_and_open_pr( "pr_url": None, } - encrypted_token = configurable.get("github_token_encrypted") - github_token = decrypt_token(encrypted_token) if encrypted_token else None + github_token = get_github_token() if not github_token: - return {"success": False, "error": "Missing GitHub token", "pr_url": None} + logger.error("commit_and_open_pr missing GitHub token for thread %s", thread_id) + return { + "success": False, + "error": "Missing GitHub token", + "pr_url": None, + } push_result = git_push(sandbox_backend, repo_dir, target_branch, github_token) if push_result.exit_code != 0: diff --git a/apps/agent/agent/utils/auth.py b/apps/agent/agent/utils/auth.py new file mode 100644 index 00000000..9077547d --- /dev/null +++ b/apps/agent/agent/utils/auth.py @@ -0,0 +1,251 @@ +"""GitHub OAuth and LangSmith authentication utilities.""" + +from __future__ import annotations + +import logging +import os +from datetime import UTC, datetime, timedelta +from typing import Any + +import httpx +import jwt +from langgraph.config import get_config +from langgraph_sdk import get_client + +from ..encryption import encrypt_token +from .linear import comment_on_linear_issue + +logger = logging.getLogger(__name__) + +client = get_client() + +LANGSMITH_API_KEY = os.environ.get("LANGSMITH_API_KEY_PROD", "") +LANGSMITH_API_URL = os.environ.get("LANGSMITH_ENDPOINT", "https://api.smith.langchain.com") +LANGSMITH_HOST_API_URL = os.environ.get("LANGSMITH_HOST_API_URL", "https://api.host.langchain.com") +GITHUB_OAUTH_PROVIDER_ID = os.environ.get("GITHUB_OAUTH_PROVIDER_ID", "") +X_SERVICE_AUTH_JWT_SECRET = os.environ.get("X_SERVICE_AUTH_JWT_SECRET", "") + +logger.debug( + "Auth env snapshot: LANGSMITH_API_KEY_PROD=%s LANGSMITH_ENDPOINT=%s " + "LANGSMITH_HOST_API_URL=%s GITHUB_OAUTH_PROVIDER_ID=%s X_SERVICE_AUTH_JWT_SECRET=%s", + "set" if LANGSMITH_API_KEY else "missing", + "set" if LANGSMITH_API_URL else "missing", + "set" if LANGSMITH_HOST_API_URL else "missing", + "set" if GITHUB_OAUTH_PROVIDER_ID else "missing", + "set" if X_SERVICE_AUTH_JWT_SECRET else "missing", +) + + +def get_service_jwt_token_for_user( + user_id: str, tenant_id: str, expiration_seconds: int = 300 +) -> str: + """Create a short-lived service JWT for authenticating as a specific user.""" + if not X_SERVICE_AUTH_JWT_SECRET: + msg = "X_SERVICE_AUTH_JWT_SECRET is not configured. Cannot generate service keys." + raise ValueError(msg) + + payload = { + "sub": user_id, + "tenant_id": tenant_id, + "iat": datetime.now(UTC), + "exp": datetime.now(UTC) + timedelta(seconds=expiration_seconds), + } + return jwt.encode(payload, X_SERVICE_AUTH_JWT_SECRET, algorithm="HS256") + + +async def get_ls_user_id_from_email(email: str) -> dict[str, str | None]: + """Get the LangSmith user ID and tenant ID from a user's email.""" + if not LANGSMITH_API_KEY: + logger.warning("LangSmith API key not configured; cannot resolve LS user for %s", email) + return {"ls_user_id": None, "tenant_id": None} + + url = f"{LANGSMITH_API_URL}/api/v1/workspaces/current/members/active" + + async with httpx.AsyncClient() as client: + try: + response = await client.get( + url, + headers={"X-API-Key": LANGSMITH_API_KEY}, + params={"emails": [email]}, + ) + response.raise_for_status() + members = response.json() + + if members and len(members) > 0: + member = members[0] + return { + "ls_user_id": member.get("ls_user_id"), + "tenant_id": member.get("tenant_id"), + } + except httpx.HTTPError: + logger.debug("HTTP error getting LangSmith user info for email") + return {"ls_user_id": None, "tenant_id": None} + + +async def get_github_token_for_user(ls_user_id: str, tenant_id: str) -> dict[str, Any]: + """Get GitHub OAuth token for a user via LangSmith agent auth.""" + if not GITHUB_OAUTH_PROVIDER_ID: + logger.error("GitHub auth failed: GITHUB_OAUTH_PROVIDER_ID is not configured") + return {"error": "GITHUB_OAUTH_PROVIDER_ID not configured"} + + try: + service_token = get_service_jwt_token_for_user(ls_user_id, tenant_id) + + headers = { + "X-Service-Key": service_token, + "X-Tenant-Id": tenant_id, + } + + payload = { + "provider": GITHUB_OAUTH_PROVIDER_ID, + "scopes": ["repo"], + "user_id": ls_user_id, + "ls_user_id": ls_user_id, + } + + async with httpx.AsyncClient() as client: + response = await client.post( + f"{LANGSMITH_HOST_API_URL}/v2/auth/authenticate", + json=payload, + headers=headers, + ) + response.raise_for_status() + response_data = response.json() + + token = response_data.get("token") + auth_url = response_data.get("url") + + if token: + return {"token": token} + if auth_url: + return {"auth_url": auth_url} + return {"error": f"Unexpected auth result: {response_data}"} + + except httpx.HTTPStatusError as e: + logger.error("GitHub auth API HTTP error: %s - %s", e.response.status_code, e.response.text) + return {"error": f"HTTP error: {e.response.status_code} - {e.response.text}"} + except Exception as e: # noqa: BLE001 + logger.error("GitHub auth API call failed: %s: %s", type(e).__name__, str(e)) + return {"error": str(e)} + + +async def resolve_github_token_from_email(email: str) -> dict[str, Any]: + """Resolve a GitHub token for a user identified by email. + + Chains get_ls_user_id_from_email -> get_github_token_for_user. + + Returns: + Dict with one of: + - {"token": str} on success + - {"auth_url": str} if user needs to authenticate via OAuth + - {"error": str} on failure; error="no_ls_user" if email not in LangSmith + """ + user_info = await get_ls_user_id_from_email(email) + ls_user_id = user_info.get("ls_user_id") + tenant_id = user_info.get("tenant_id") + + if not ls_user_id or not tenant_id: + logger.warning( + "No LangSmith user found for email %s (ls_user_id=%s, tenant_id=%s)", + email, + ls_user_id, + tenant_id, + ) + return {"error": "no_ls_user", "email": email} + + auth_result = await get_github_token_for_user(ls_user_id, tenant_id) + return auth_result + + +async def leave_failure_comment( + source: str, + message: str, +) -> None: + """Leave an auth failure comment for the appropriate source.""" + if source == "linear": + config = get_config() + configurable = config.get("configurable", {}) + linear_issue = configurable.get("linear_issue", {}) + issue_id = linear_issue.get("id") if isinstance(linear_issue, dict) else None + if issue_id: + logger.info( + "Posting auth failure comment to Linear issue %s (source=%s)", + issue_id, + source, + ) + await comment_on_linear_issue(issue_id, message) + return + raise ValueError(f"Unknown source: {source}") + + +async def persist_encrypted_github_token(thread_id: str, token: str) -> str: + """Encrypt a GitHub token and store it on the thread metadata.""" + encrypted = encrypt_token(token) + await client.threads.update( + thread_id=thread_id, + metadata={"github_token_encrypted": encrypted}, + ) + return encrypted + + +async def save_encrypted_token_from_email( + email: str | None, + source: str, +) -> tuple[str, str]: + """Resolve, encrypt, and store a GitHub token based on user email.""" + config = get_config() + configurable = config.get("configurable", {}) + thread_id = configurable.get("thread_id") + if not thread_id: + raise ValueError("GitHub auth failed: missing thread_id") + if not email: + message = ( + "❌ **GitHub Auth Error**\n\n" + "Failed to authenticate with GitHub: missing_user_email\n\n" + "Please try again or contact support." + ) + await leave_failure_comment(source, message) + raise ValueError("GitHub auth failed: missing user_email") + + user_info = await get_ls_user_id_from_email(email) + ls_user_id = user_info.get("ls_user_id") + tenant_id = user_info.get("tenant_id") + if not ls_user_id or not tenant_id: + message = ( + "🔐 **GitHub Authentication Required**\n\n" + f"Could not find a LangSmith account for **{email}**.\n\n" + "Please ensure this email is invited to the main LangSmith organization. " + "If your Linear account uses a different email than your LangSmith account, " + "you may need to update one of them to match.\n\n" + "Once your email is added to LangSmith, " + "reply to this issue mentioning @openswe to retry." + ) + await leave_failure_comment(source, message) + raise ValueError(f"No ls_user_id found from email {email}") + + auth_result = await get_github_token_for_user(ls_user_id, tenant_id) + auth_url = auth_result.get("auth_url") + if auth_url: + message = ( + "🔐 **GitHub Authentication Required**\n\n" + "To allow the Open SWE agent to work on this issue, " + "please authenticate with GitHub by clicking the link below:\n\n" + f"[Authenticate with GitHub]({auth_url})\n\n" + "Once authenticated, reply to this issue mentioning @openswe to retry." + ) + await leave_failure_comment(source, message) + raise ValueError("User not authenticated.") + + token = auth_result.get("token") + if not token: + error = auth_result.get("error", "unknown") + message = ( + "❌ **GitHub Auth Error**\n\n" + f"Failed to authenticate with GitHub: {error}\n\n" + "Please try again or contact support." + ) + await leave_failure_comment(source, message) + raise ValueError(f"No token found: {error}") + + encrypted = await persist_encrypted_github_token(thread_id, token) + return token, encrypted diff --git a/apps/agent/agent/utils/github_token.py b/apps/agent/agent/utils/github_token.py new file mode 100644 index 00000000..6afc3109 --- /dev/null +++ b/apps/agent/agent/utils/github_token.py @@ -0,0 +1,36 @@ +"""GitHub token lookup utilities.""" + +from __future__ import annotations + +import asyncio +import logging + +from langgraph.config import get_config +from langgraph_sdk import get_client + +from ..encryption import decrypt_token + +logger = logging.getLogger(__name__) + +client = get_client() + + +def get_github_token() -> str | None: + """Resolve a GitHub token from config metadata or thread metadata.""" + config = get_config() + configurable = config.get("configurable", {}) + thread_id = configurable.get("thread_id") + + encrypted_token = None + metadata = config.get("metadata", {}) + if isinstance(metadata, dict): + encrypted_token = metadata.get("github_token_encrypted") + if not encrypted_token and thread_id: + try: + thread = asyncio.run(client.threads.get(thread_id)) + thread_metadata = (thread or {}).get("metadata", {}) + if isinstance(thread_metadata, dict): + encrypted_token = thread_metadata.get("github_token_encrypted") + except Exception: # noqa: BLE001 + logger.exception("Failed to fetch thread metadata for %s", thread_id) + return decrypt_token(encrypted_token) if encrypted_token else None diff --git a/apps/agent/agent/webapp.py b/apps/agent/agent/webapp.py index 24467688..7c14877f 100644 --- a/apps/agent/agent/webapp.py +++ b/apps/agent/agent/webapp.py @@ -5,19 +5,14 @@ import hmac import json import logging import os -from datetime import UTC, datetime, timedelta from typing import Any import httpx -import jwt from fastapi import BackgroundTasks, FastAPI, HTTPException, Request from langchain_core.messages.content import create_text_block from langgraph_sdk import get_client -# Local import for encryption -from .encryption import encrypt_token from .utils.comments import get_recent_comments -from .utils.linear import comment_on_linear_issue from .utils.multimodal import dedupe_urls, extract_image_urls, fetch_image_block logger = logging.getLogger(__name__) @@ -30,48 +25,8 @@ LANGGRAPH_URL = os.environ.get("LANGGRAPH_URL") or os.environ.get( "LANGGRAPH_URL_PROD", "http://localhost:2024" ) -LANGSMITH_API_KEY = os.environ.get("LANGSMITH_API_KEY_PROD", "") -LANGSMITH_API_URL = os.environ.get("LANGSMITH_ENDPOINT", "https://api.smith.langchain.com") - -GITHUB_OAUTH_PROVIDER_ID = os.environ.get("GITHUB_OAUTH_PROVIDER_ID", "") - LINEAR_API_KEY = os.environ.get("LINEAR_API_KEY", "") -X_SERVICE_AUTH_JWT_SECRET = os.environ.get("X_SERVICE_AUTH_JWT_SECRET", "") - - -def get_service_jwt_token_for_user( - user_id: str, tenant_id: str, expiration_seconds: int = 300 -) -> str: - """Create a short-lived service JWT for authenticating as a specific user. - - Args: - tenant_id: The LangSmith tenant ID to associate with the token - user_id: The LangSmith user ID to associate with the token - expiration_seconds: Token expiration time in seconds (default: 5 minutes) - - Returns: - JWT token string - - Raises: - ValueError: If X_SERVICE_AUTH_JWT_SECRET is not configured - """ - if not X_SERVICE_AUTH_JWT_SECRET: - msg = "X_SERVICE_AUTH_JWT_SECRET is not configured. Cannot generate service keys." - raise ValueError(msg) - - exp_datetime = datetime.now(tz=UTC) + timedelta(seconds=expiration_seconds) - exp = int(exp_datetime.timestamp()) - - payload = { - "sub": "unspecified", - "exp": exp, - "tenant_id": tenant_id, - "user_id": user_id, - } - - return jwt.encode(payload, X_SERVICE_AUTH_JWT_SECRET, algorithm="HS256") - LINEAR_TEAM_TO_REPO: dict[str, dict[str, Any] | dict[str, str]] = { "Brace's test workspace": {"owner": "langchain-ai", "name": "open-swe"}, @@ -136,100 +91,6 @@ def get_repo_config_from_team_mapping( return {"owner": "langchain-ai", "name": "langchainplus"} -async def get_ls_user_id_from_email(email: str) -> dict[str, str | None]: - """Get the LangSmith user ID and tenant ID from a user's email. - - Args: - email: The user's email address - - Returns: - Dict with 'ls_user_id' and 'tenant_id' keys (values may be None if not found) - """ - if not LANGSMITH_API_KEY: - return {"ls_user_id": None, "tenant_id": None} - - url = f"{LANGSMITH_API_URL}/api/v1/workspaces/current/members/active" - - async with httpx.AsyncClient() as client: - try: - response = await client.get( - url, - headers={"X-API-Key": LANGSMITH_API_KEY}, - params={"emails": [email]}, - ) - response.raise_for_status() - members = response.json() - - if members and len(members) > 0: - member = members[0] - return { - "ls_user_id": member.get("ls_user_id"), - "tenant_id": member.get("tenant_id"), - } - except httpx.HTTPError: - logger.debug("HTTP error getting LangSmith user info for email") - return {"ls_user_id": None, "tenant_id": None} - - -LANGSMITH_HOST_API_URL = os.environ.get("LANGSMITH_HOST_API_URL", "https://api.host.langchain.com") - - -async def get_github_token_for_user(ls_user_id: str, tenant_id: str) -> dict[str, Any]: - """Get GitHub OAuth token for a user via LangSmith agent auth. - - Args: - ls_user_id: The LangSmith user ID - tenant_id: The LangSmith tenant ID - - Returns: - Dict with either 'token' key or 'auth_url' key - """ - if not GITHUB_OAUTH_PROVIDER_ID: - logger.error("GitHub auth failed: GITHUB_OAUTH_PROVIDER_ID is not configured") - return {"error": "GITHUB_OAUTH_PROVIDER_ID not configured"} - - try: - service_token = get_service_jwt_token_for_user(ls_user_id, tenant_id) - - headers = { - "X-Service-Key": service_token, - "X-Tenant-Id": tenant_id, - "X-User-Id": ls_user_id, - } - - payload = { - "provider": GITHUB_OAUTH_PROVIDER_ID, - "scopes": ["repo"], - "user_id": ls_user_id, - "ls_user_id": ls_user_id, - } - - async with httpx.AsyncClient() as client: - response = await client.post( - f"{LANGSMITH_HOST_API_URL}/v2/auth/authenticate", - json=payload, - headers=headers, - ) - response.raise_for_status() - response_data = response.json() - - token = response_data.get("token") - auth_url = response_data.get("url") - - if token: - return {"token": token} - if auth_url: - return {"auth_url": auth_url} - return {"error": f"Unexpected auth result: {response_data}"} - - except httpx.HTTPStatusError as e: - logger.error("GitHub auth API HTTP error: %s - %s", e.response.status_code, e.response.text) - return {"error": f"HTTP error: {e.response.status_code} - {e.response.text}"} - except Exception as e: # noqa: BLE001 - logger.error("GitHub auth API call failed: %s: %s", type(e).__name__, str(e)) - return {"error": str(e)} - - async def react_to_linear_comment(comment_id: str, emoji: str = "👀") -> bool: """Add an emoji reaction to a Linear comment. @@ -479,59 +340,7 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915 user_email = assignee.get("email") user_name = user_name or assignee.get("name") - user_mention = f"@{user_name}" if user_name else "" - - logger.info( - "User email: %s, GITHUB_OAUTH_PROVIDER_ID set: %s", - user_email, - bool(GITHUB_OAUTH_PROVIDER_ID), - ) - - github_token = None - if user_email and GITHUB_OAUTH_PROVIDER_ID: - user_info = await get_ls_user_id_from_email(user_email) - ls_user_id = user_info.get("ls_user_id") - tenant_id = user_info.get("tenant_id") - logger.info( - "LangSmith user ID for %s: %s, tenant_id: %s", user_email, ls_user_id, tenant_id - ) - - if ls_user_id and tenant_id: - auth_result = await get_github_token_for_user(ls_user_id, tenant_id) - logger.info("Auth result keys: %s", list(auth_result.keys())) - - if "token" in auth_result: - github_token = auth_result["token"] - logger.info("GitHub token obtained for user %s", user_email) - elif "auth_url" in auth_result: - auth_url = auth_result["auth_url"] - logger.info("GitHub auth required for user %s, sending auth URL", user_email) - comment = ( - f"🔐 **GitHub Authentication Required** {user_mention}\n\n" - "To allow the Open SWE agent to work on this issue, " - "please authenticate with GitHub by clicking the link below:\n\n" - f"[Authenticate with GitHub]({auth_url})\n\n" - "Once authenticated, reply to this issue mentioning @openswe to retry." - ) - - await comment_on_linear_issue(issue_id, comment) - return - else: - logger.warning("Auth result has neither token nor auth_url: %s", auth_result) - else: - logger.warning("User %s not found in LangSmith workspace", user_email) - comment = ( - f"🔐 **GitHub Authentication Required** {user_mention}\n\n" - f"Could not find a LangSmith account for **{user_email}**.\n\n" - "Please ensure this email is invited to the main LangSmith organization. " - "If your Linear account uses a different email than your LangSmith account, " - "you may need to update one of them to match.\n\n" - "Once your email is added to LangSmith, " - "reply to this issue mentioning @openswe to retry." - ) - - await comment_on_linear_issue(issue_id, comment) - return + logger.info("User email for issue %s: %s", issue_id, user_email) title = full_issue.get("title", "No title") description = full_issue.get("description") or "No description" @@ -667,67 +476,41 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915 "linear_issue_number": linear_issue_number, "triggering_user_name": user_name or "", }, + "user_email": user_email, + "source": "linear", } - if github_token: - configurable["github_token_encrypted"] = encrypt_token(github_token) - logger.info("Checking if thread %s is active before creating run", thread_id) - thread_active = await is_thread_active(thread_id) - logger.info("Thread %s active status: %s", thread_id, thread_active) + logger.info("Checking if thread %s is active before creating run", thread_id) + thread_active = await is_thread_active(thread_id) + logger.info("Thread %s active status: %s", thread_id, thread_active) - if thread_active: - logger.info( - "Thread %s is active (busy), will queue message instead of creating run", - thread_id, - ) + if thread_active: + logger.info( + "Thread %s is active (busy), will queue message instead of creating run", + thread_id, + ) - queued_payload = {"text": prompt, "image_urls": image_urls} - queued = await queue_message_for_thread( - thread_id=thread_id, - message_content=queued_payload, - ) + queued_payload = {"text": prompt, "image_urls": image_urls} + queued = await queue_message_for_thread( + thread_id=thread_id, + message_content=queued_payload, + ) - if queued: - logger.info( - "Message queued for thread %s, will be processed by middleware", thread_id - ) - else: - logger.error("Failed to queue message for thread %s", thread_id) + if queued: + logger.info("Message queued for thread %s, will be processed by middleware", thread_id) else: - logger.info("Creating LangGraph run for thread %s", thread_id) - langgraph_client = get_client(url=LANGGRAPH_URL) - await langgraph_client.runs.create( - thread_id, - "agent", - input={"messages": [{"role": "user", "content": content_blocks}]}, - config={"configurable": configurable}, - if_not_exists="create", - ) - logger.info("LangGraph run created successfully for thread %s", thread_id) + logger.error("Failed to queue message for thread %s", thread_id) else: - logger.warning("No GitHub token available, cannot create run for issue %s", issue_id) - if not user_email: - comment = ( - f"🔐 **GitHub Authentication Required** {user_mention}\n\n" - "Could not determine the user email from this issue. " - "Please ensure your Linear account has an email address configured.\n\n" - "Reply to this issue mentioning @openswe to retry." - ) - elif not GITHUB_OAUTH_PROVIDER_ID: - comment = ( - f"❌ **Configuration Error** {user_mention}\n\n" - "The Open SWE agent is not properly configured (missing GitHub OAuth provider).\n\n" - "Please contact your administrator." - ) - else: - comment = ( - f"🔐 **GitHub Authentication Required** {user_mention}\n\n" - "Unable to authenticate with GitHub. " - "Please ensure you have connected your GitHub account in LangSmith.\n\n" - "Reply to this issue mentioning @openswe to retry." - ) - - await comment_on_linear_issue(issue_id, comment) + logger.info("Creating LangGraph run for thread %s", thread_id) + langgraph_client = get_client(url=LANGGRAPH_URL) + await langgraph_client.runs.create( + thread_id, + "agent", + input={"messages": [{"role": "user", "content": content_blocks}]}, + config={"configurable": configurable}, + if_not_exists="create", + ) + logger.info("LangGraph run created successfully for thread %s", thread_id) def verify_linear_signature(body: bytes, signature: str, secret: str) -> bool: