Files
雷电芽衣 407e4628a0 chore(python): audit providers kimi grok qwen letta
Move the four providers onto _runtime and _shell_command; expected
runtime scripts take the runtime config; add unique_preconditions.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-02 23:58:10 -04:00

222 lines
6.8 KiB
Python

"""GitHub Copilot CLI source discovery and managed runtime validation."""
from __future__ import annotations
from dataclasses import dataclass
import os
from typing import List, Optional, Tuple
from ._jsonc import parse_jsonc
from ._managed_files import (
FileSnapshot,
MAX_SOURCE_BYTES,
physical_directory_exists,
read_physical_file,
)
from ._runtime import expected_runtime_scripts, runtime_contract_exists
from ._strict_json import JsonObject, parse_json_object
from .copilot_contract import (
AGENT_KEY,
CONFIG_FILE,
CopilotDocument,
CopilotSources,
RuntimeContract,
SourcePrecondition,
create_document,
parse_document,
)
@dataclass(frozen=True)
class SettingsLayer:
file_path: str
label: str
jsonc: bool
@dataclass(frozen=True)
class CopilotPaths:
copilot_home: str
user_hooks_directory: str
user_hook_path: str
legacy_hook_path: str
settings_layers: Tuple[SettingsLayer, ...]
inspected_directories: Tuple[Tuple[str, str], ...]
@dataclass(frozen=True)
class _ParsedSettingsLayer:
layer: SettingsLayer
disable_all_hooks: Optional[bool]
snapshot: Optional[FileSnapshot]
def resolve_copilot_paths(home_dir: Optional[str] = None) -> CopilotPaths:
home = home_dir if home_dir is not None else os.path.expanduser("~")
override = os.environ.get("COPILOT_HOME")
copilot_home = override if override else os.path.join(home, ".copilot")
project = os.getcwd()
github = os.path.join(project, ".github")
github_copilot = os.path.join(github, "copilot")
github_hooks = os.path.join(github, "hooks")
claude = os.path.join(project, ".claude")
user_hooks = os.path.join(copilot_home, "hooks")
return CopilotPaths(
copilot_home=copilot_home,
user_hooks_directory=user_hooks,
user_hook_path=os.path.join(user_hooks, CONFIG_FILE),
legacy_hook_path=os.path.join(github_hooks, "hooks.json"),
settings_layers=(
SettingsLayer(
os.path.join(copilot_home, "config.json"),
"legacy Copilot user config",
False,
),
SettingsLayer(
os.path.join(copilot_home, "settings.json"),
"Copilot user settings",
True,
),
SettingsLayer(
os.path.join(claude, "settings.json"),
"Claude repository settings",
True,
),
SettingsLayer(
os.path.join(claude, "settings.local.json"),
"Claude local settings",
True,
),
SettingsLayer(
os.path.join(github_copilot, "settings.json"),
"Copilot repository settings",
True,
),
SettingsLayer(
os.path.join(github_copilot, "settings.local.json"),
"Copilot local settings",
True,
),
),
inspected_directories=(
(project, "Copilot working directory"),
(copilot_home, "COPILOT_HOME"),
(user_hooks, "Copilot user hooks directory"),
(github, "GitHub configuration directory"),
(github_hooks, "GitHub repository hooks directory"),
(github_copilot, "Copilot repository settings directory"),
(claude, "Claude repository settings directory"),
),
)
def _inspect_directories(locations: Tuple[Tuple[str, str], ...]) -> None:
for directory, label in locations:
physical_directory_exists(directory, label)
def _read_hook_document(
file_path: str, label: str
) -> Optional[CopilotDocument]:
snapshot = read_physical_file(file_path, label, MAX_SOURCE_BYTES)
return None if snapshot is None else parse_document(file_path, snapshot, label)
def _parse_settings(raw: str, layer: SettingsLayer) -> JsonObject:
if not raw.strip():
return {}
label = f"{layer.label} at {layer.file_path}"
value = (
parse_jsonc(raw, label, allow_trailing_commas=True)
if layer.jsonc
else parse_json_object(raw, label)
)
if not isinstance(value, dict):
raise ValueError(f"{label} must contain a JSON object")
return value
def _read_settings_layer(layer: SettingsLayer) -> _ParsedSettingsLayer:
snapshot = read_physical_file(
layer.file_path, layer.label, MAX_SOURCE_BYTES
)
if snapshot is None:
return _ParsedSettingsLayer(layer, None, None)
root = _parse_settings(snapshot.contents, layer)
disabled = root.get("disableAllHooks")
if "disableAllHooks" in root and not isinstance(disabled, bool):
raise ValueError(
f'{layer.label} at {layer.file_path} field "disableAllHooks" '
"must be a boolean"
)
return _ParsedSettingsLayer(layer, disabled, snapshot)
def _effective_disabled_source(
layers: List[_ParsedSettingsLayer],
) -> Optional[str]:
disabled_by: Optional[str] = None
for item in layers:
source = f"{item.layer.label} at {item.layer.file_path}"
if item.disable_all_hooks is True:
disabled_by = source
elif item.disable_all_hooks is False:
disabled_by = None
return disabled_by
def read_sources(home_dir: Optional[str] = None) -> CopilotSources:
paths = resolve_copilot_paths(home_dir)
_inspect_directories(paths.inspected_directories)
user = _read_hook_document(
paths.user_hook_path, "GitHub Copilot user hooks"
)
legacy = _read_hook_document(
paths.legacy_hook_path, "GitHub Copilot legacy project hooks"
)
layers = [_read_settings_layer(layer) for layer in paths.settings_layers]
user_document = user or create_document(paths.user_hook_path)
disabled_by = (
f"GitHub Copilot user hooks at {paths.user_hook_path}"
if user_document.hooks_disabled
else _effective_disabled_source(layers)
)
preconditions = tuple(
SourcePrecondition(
item.layer.file_path,
item.layer.label,
item.snapshot,
)
for item in layers
)
return CopilotSources(
user_document,
legacy,
disabled_by,
preconditions,
)
def require_hooks_enabled(sources: CopilotSources) -> None:
if sources.disabled_by:
raise ValueError(
"GitHub Copilot hooks are disabled by "
f"{sources.disabled_by}; set disableAllHooks to false before installation"
)
def _expected_scripts(agent_id: str, _config: JsonObject) -> Tuple[str, str]:
return expected_runtime_scripts(AGENT_KEY, agent_id)
def runtime_files_exist(contracts: List[RuntimeContract]) -> bool:
return any(
runtime_contract_exists(
contract,
AGENT_KEY,
"Copilot",
_expected_scripts,
)
for contract in contracts
)