Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions validation/ai_checker/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ py_library(
name = "copilot_langchain",
srcs = [
"src/copilot_adapter/__init__.py",
"src/copilot_adapter/_client_manager.py",
"src/copilot_adapter/_errors.py",
"src/copilot_adapter/_message_converter.py",
"src/copilot_adapter/_preflight.py",
"src/copilot_adapter/_tool_converter.py",
"src/copilot_adapter/copilot_langchain.py",
],
imports = ["src"],
Expand Down
2 changes: 1 addition & 1 deletion validation/ai_checker/ai_checker.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ fi
_COMMON_AI_TEST_ATTRS = {
"model": attr.string(
doc = "AI model name to use for analysis.",
default = "anthropic/claude-sonnet-4-5",
default = "claude-sonnet-4.6",
),
"score_threshold": attr.string(
doc = "Minimum average score required to pass the test (0-10).",
Expand Down
26 changes: 15 additions & 11 deletions validation/ai_checker/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,13 @@ charset-normalizer==3.4.4 \
--hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \
--hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608
# via requests
github-copilot-sdk==0.1.25 \
--hash=sha256:13ef99fa8c709c5f80d820672bf36ee9176bc33f0efce6a2b5cbf6d1bb2369e8 \
--hash=sha256:1a90ee583309ff308fea42f9edec61203645a33ca1d3dc42953628fb8c3eda07 \
--hash=sha256:5249a63d1ac1e4d325c70c9902e81327b0baca53afa46010f52ac3fd3b5a111b \
--hash=sha256:7af33d3afbe09a78dfc9d65a843526e47aba15631e90926c42a21a200fab12da \
--hash=sha256:bc74a3d08ee45313ac02a3f7159c583ec41fc16090ec5f27f88c4b737f03139e \
--hash=sha256:d32c3fc2c393f70923a645a133607da2e562d078b87437f499100d5bb8c1902f
github-copilot-sdk==0.3.0 \
--hash=sha256:7e241d9b00ebf8bb4d10b2d6101c75fcef38de04d144d729e07fa48394270ee1 \
--hash=sha256:93b07c46f60cebbbb003d5bddba22eab886849b1d052b98037b52b6434a5bc07 \
--hash=sha256:b591546d789f9f8243fb59ca71b08cb0bb1dbec818fbef060c3830c6787de2c8 \
--hash=sha256:c5712d57a2c6291b805c79e039c55c48d858034b1a37fc8e1653925403a028e9 \
--hash=sha256:ed8f27989158824c754d7febb473bdf25744a1e6bc07a06f114f7e7deebd2c22 \
--hash=sha256:f4d98a67b8f038885ddd38bd7033d1ac20c3010f04c72ee0fc74ba4984b69ffa
# via -r validation/ai_checker/requirements.txt.in
h11==0.16.0 \
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
Expand Down Expand Up @@ -173,10 +173,14 @@ jsonpointer==3.0.0 \
--hash=sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942 \
--hash=sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef
# via jsonpatch
langchain-core==1.2.13 \
--hash=sha256:b31823e28d3eff1e237096d0bd3bf80c6f9624eb471a9496dbfbd427779f8d82 \
--hash=sha256:d2773d0d0130a356378db9a858cfeef64c3d64bc03722f1d4d6c40eb46fdf01b
langchain-core==1.4.0 \
--hash=sha256:1dc341eed802ed9c117c0df3923c991e5e9e226571e5725c194eeb5bd93d1a7f \
--hash=sha256:23cbbdb46e38ddd1dd5247e6167e96013eae74bea4c5949c550809970a9e565c
# via -r validation/ai_checker/requirements.txt.in
langchain-protocol==0.0.15 \
--hash=sha256:461eb794358f83d5e42635a5797799ffec7b4702314e34edf73ac21e75d3ef79 \
--hash=sha256:9ab2d11ee73944754f10e037e717098d3a6796f0e58afa9cadda6154e7655ade
# via langchain-core
langsmith==0.6.3 \
--hash=sha256:33246769c0bb24e2c17e0c34bb21931084437613cd37faf83bd0978a297b826f \
--hash=sha256:44fdf8084165513e6bede9dda715e7b460b1b3f57ac69f2ca3f03afa911233ec
Expand Down Expand Up @@ -519,8 +523,8 @@ typing-extensions==4.15.0 \
--hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
# via
# anyio
# github-copilot-sdk
# langchain-core
# langchain-protocol
# pydantic
# pydantic-core
# typing-inspection
Expand Down
4 changes: 2 additions & 2 deletions validation/ai_checker/requirements.txt.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ pydantic
pyyaml

# LangChain + GitHub Copilot SDK
github-copilot-sdk>=0.1.23
langchain-core>=1.2.9
github-copilot-sdk>=0.3.0
langchain-core>=1.4.0
2 changes: 1 addition & 1 deletion validation/ai_checker/src/ai_checker/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@
"""

# Default AI model to use for all analysis operations
DEFAULT_MODEL = "anthropic/claude-sonnet-4-5"
DEFAULT_MODEL = "claude-sonnet-4.6"
18 changes: 18 additions & 0 deletions validation/ai_checker/src/copilot_adapter/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
"""Public API for the copilot_adapter package."""

from .copilot_langchain import ChatCopilot
from ._errors import CopilotSetupError

__all__ = ["ChatCopilot", "CopilotSetupError"]
237 changes: 237 additions & 0 deletions validation/ai_checker/src/copilot_adapter/_client_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
"""Lifecycle management for the Copilot CLI subprocess and SDK client."""

from __future__ import annotations

import logging
from typing import Any, Optional

from copilot import CopilotClient, SubprocessConfig

from ._errors import CopilotSetupError
from ._preflight import (
check_auth_sources,
check_cli_binary,
check_environment,
describe_auth_sources,
resolve_copilot_cli_path,
)

logger = logging.getLogger(__name__)


class CopilotClientManager:
"""Owns the lifecycle of a single CopilotClient / CLI subprocess.

Responsibilities:
- Resolve the CLI binary path (rules_python copy_executables workaround)
- Run pre-flight checks before spawning the process
- Start the subprocess and verify authentication
- Expose the live client for callers
- Shut the process down cleanly on close

This class is intentionally not a Pydantic model — it holds mutable
runtime state that must not be serialised.
"""

def __init__(self, copilot_client_options: dict[str, Any] | None = None) -> None:
self._options: dict[str, Any] = dict(copilot_client_options or {})
self._client: Optional[CopilotClient] = None
self._started: bool = False

# ------------------------------------------------------------------
# Public interface
# ------------------------------------------------------------------

async def ensure_client(self) -> CopilotClient:
"""Return a started, authenticated CopilotClient.

Creates and starts the client on the first call; subsequent calls
return the cached instance immediately.

Pre-flight sequence (runs once, before the CLI is spawned):
1. Resolve the CLI binary path
2. Validate the binary exists and is executable
3. Hard-fail if no auth source is available at all
4. Warn about missing $HOME / HTTPS_PROXY (non-fatal)
5. Start the CLI subprocess
6. Verify authentication via get_auth_status()

Raises:
CopilotSetupError: With a detailed, actionable message for any
failure that prevents the CLI from being used.
"""
if self._client is None:
self._client = self._create_client()

if not self._started:
await self._start_and_verify()

return self._client

async def close(self) -> None:
"""Stop the CLI subprocess if it is running."""
if self._client and self._started:
await self._client.stop()
self._started = False

# ------------------------------------------------------------------
# Internal helpers
# ------------------------------------------------------------------

def _create_client(self) -> CopilotClient:
"""Run pre-flight checks and construct (but do not start) the client."""
opts = dict(self._options)

# --- Resolve CLI binary path ----------------------------------
if "cli_path" not in opts and "cli_url" not in opts:
resolved = resolve_copilot_cli_path()
if resolved:
opts["cli_path"] = resolved
logger.info("Resolved Copilot CLI path: %s", resolved)
else:
logger.warning(
"Could not find copilot_cli (copy_executables target). "
"Falling back to bundled binary — this may fail with "
"PermissionError if the executable bit was stripped."
)

# --- Check binary --------------------------------------------
cli_path = opts.get("cli_path")
if cli_path:
problems = check_cli_binary(cli_path)
if problems:
raise CopilotSetupError(
"Copilot CLI binary check failed:\n"
+ "\n".join(f" - {p}" for p in problems)
)

# --- Hard-fail if no auth source available -------------------
auth_problems = check_auth_sources()
if auth_problems:
raise CopilotSetupError(
"Copilot authentication pre-flight check failed — "
"the CLI process will not be started:\n"
+ "\n".join(f" - {p}" for p in auth_problems)
+ "\n\n"
+ describe_auth_sources()
)

# --- Warn about non-fatal env issues -------------------------
env_problems = check_environment()
if env_problems:
logger.warning(
"Environment issues detected:\n%s\n%s",
"\n".join(f" - {p}" for p in env_problems),
describe_auth_sources(),
)

logger.info("Starting CopilotClient...\n%s", describe_auth_sources())
_subprocess_fields = frozenset(
{
"cli_path",
"cli_args",
"cwd",
"use_stdio",
"port",
"log_level",
"env",
"github_token",
"use_logged_in_user",
"telemetry",
"session_fs",
"session_idle_timeout_seconds",
}
)
subprocess_kwargs = {k: v for k, v in opts.items() if k in _subprocess_fields}
return CopilotClient(SubprocessConfig(**subprocess_kwargs))

async def _start_and_verify(self) -> None:
"""Start the CLI subprocess and verify authentication."""
assert self._client is not None

try:
await self._client.start()
except PermissionError as exc:
raise CopilotSetupError(
f"PermissionError starting Copilot CLI: {exc}\n"
" The CLI binary is not executable. Make sure\n"
" pip.whl_mods / copy_executables is configured in MODULE.bazel\n"
" to create an executable copy of copilot/bin/copilot."
) from exc
except RuntimeError as exc:
if "timeout" in str(exc).lower() or "Timeout" in str(exc):
raise CopilotSetupError(
f"Timeout starting Copilot CLI server: {exc}\n"
" The CLI started but did not become ready in time.\n"
" This usually means the CLI cannot authenticate.\n\n"
+ describe_auth_sources()
+ "\n\n"
" Possible fixes:\n"
" 1. Run 'copilot' in a terminal and sign in interactively.\n"
" 2. Set COPILOT_GITHUB_TOKEN (or GH_TOKEN / GITHUB_TOKEN)\n"
" and pass it via --action_env=COPILOT_GITHUB_TOKEN.\n"
" 3. Ensure HOME is available in the action environment\n"
" (use_default_shell_env = True in the Bazel rule).\n"
" See: https://github.com/github/copilot-sdk/blob/main/docs/auth/index.md"
) from exc
raise
except Exception as exc:
raise CopilotSetupError(
f"Failed to start CopilotClient: {type(exc).__name__}: {exc}\n\n"
+ describe_auth_sources()
) from exc

self._started = True
await self._verify_auth()

async def _verify_auth(self) -> None:
"""Log the result of get_auth_status() as a diagnostic; never hard-fail.

Rationale: get_auth_status() can return isAuthenticated=False even when
the CLI is fully functional — for example:
- The auth state is resolved lazily on the first real request.
- GitHub Enterprise hosts (*.ghe.com) may not be reflected immediately.
- There is a brief window after start() where the status is not yet set.

A false-positive hard-fail here would block valid requests. The actual
LLM call (send_and_wait) will fail with a clear error if auth is truly
broken, so we demote this check to a warning-only diagnostic.
"""
assert self._client is not None
try:
auth_status = await self._client.get_auth_status()
# The SDK uses camelCase on some versions, snake_case on others.
is_auth = getattr(auth_status, "isAuthenticated", None) or getattr(
auth_status, "is_authenticated", None
)
if is_auth:
user = getattr(auth_status, "login", "unknown")
logger.info("Copilot authenticated as: %s", user)
else:
# Log as a warning only — do not raise. The CLI may still work.
logger.warning(
"get_auth_status() reports isAuthenticated=False — "
"continuing anyway; auth may be resolved on first request.\n"
" Auth status: %s\n%s",
auth_status,
describe_auth_sources(),
)
except Exception as exc:
# get_auth_status itself failed — log but do not block.
logger.warning(
"Could not verify auth status (non-fatal): %s: %s",
type(exc).__name__,
exc,
)
26 changes: 26 additions & 0 deletions validation/ai_checker/src/copilot_adapter/_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
"""Shared error types and constants for the Copilot adapter."""

from __future__ import annotations

# Auth-related environment variables checked by the Copilot CLI (priority order)
AUTH_ENV_VARS: list[str] = [
"COPILOT_GITHUB_TOKEN", # Recommended for explicit Copilot usage
"GH_TOKEN", # GitHub CLI compatible
"GITHUB_TOKEN", # GitHub Actions compatible
]


class CopilotSetupError(RuntimeError):
"""Raised when the Copilot SDK environment is not correctly configured."""
Loading
Loading