chore: resolve github token in get_agent and read from thread metadata (#1002)

* chore: resolve github token in get_agent and read from thread metadata

* feat: add linear_comment tool and fix middleware PR response

* refactor: simplify github token resolution flow

* linting

* Apply suggestion from @bracesproul

Co-authored-by: Brace Sproul <braceasproul@gmail.com>

* Apply suggestion from @bracesproul

Co-authored-by: Brace Sproul <braceasproul@gmail.com>

* refactor: simplify auth helpers and token flow

* linting

* open_pr changes

* Update open_pr.py

* refactor: centralize github token helpers and config lookup

* Apply suggestions from code review

---------

Co-authored-by: Brace Sproul <braceasproul@gmail.com>
This commit is contained in:
Aran Yogesh 2026-03-04 15:57:03 -08:00 • committed by GitHub
parent 3dee480e82
commit b5951413d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 346 additions and 258 deletions

View file

@ -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:
- <change 2>
```
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 = (

View file

@ -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)

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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: