Files
tracer-cloud--opensre/surfaces/interactive_shell/runtime/startup/first_launch_github.py
T
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

304 lines
10 KiB
Python

"""First-launch GitHub login gate.
On the first interactive launch of ``opensre`` (all platforms), the user is
prompted to sign in to GitHub via device flow unless they skip, are in CI/CD, a
test harness, or a non-interactive session. The sign-in runs the hosted GitHub
MCP setup, persists the integration, and propagates the authenticated GitHub
username to PostHog.
Escape hatch: ``OPENSRE_SKIP_GITHUB_LOGIN=1`` bypasses the gate so a GitHub
outage or a disabled device flow can never permanently lock anyone out. The gate
is also auto-bypassed in CI/test environments and when stdin is not a TTY.
"""
from __future__ import annotations
import logging
import os
import sys
import time
from rich.console import Console
from rich.markup import escape
from config.repl_config import read_github_login_deferred, write_github_login_deferred
from platform.analytics.cli import capture_github_login_completed
from platform.analytics.source import is_test_run
from platform.terminal.theme import DEVICE_CODE
from surfaces.interactive_shell.ui import repl_tty_interactive
_SKIP_ENV_VAR = "OPENSRE_SKIP_GITHUB_LOGIN"
_TRUTHY = frozenset({"1", "true", "yes", "on"})
_SIGN_IN_CHOICE = "sign_in"
_SKIP_CHOICE = "skip"
def _skip_requested() -> bool:
return os.getenv(_SKIP_ENV_VAR, "").strip().lower() in _TRUTHY
def _github_login_explicitly_bypassed() -> bool:
"""Cheap check for contexts where gate errors should not block startup."""
if _skip_requested():
return True
if os.getenv("OPENSRE_INVESTIGATION_SOURCE", "").strip().lower() == "test":
return True
if os.getenv("OPENSRE_IS_TEST", "0").strip() == "1":
return True
if os.getenv("PYTEST_CURRENT_TEST"):
return True
if os.getenv("GITHUB_ACTIONS", "").strip().lower() == "true":
return True
ci_value = os.getenv("CI", "").strip().lower()
if ci_value in {"1", "true", "yes"}:
return True
try:
return not sys.stdin.isatty()
except Exception:
return True
def _github_already_configured() -> bool:
from integrations.github.mcp import github_integration_is_configured
return github_integration_is_configured()
def should_require_github_login() -> bool:
"""Return True when the first-launch GitHub login prompt must run now."""
if _skip_requested():
return False
if read_github_login_deferred():
return False
if is_test_run():
return False
if not repl_tty_interactive():
return False
# GitHub being configured is the authoritative bypass. We intentionally do
# NOT consult a first-launch "completion" marker here: a stale marker must
# never let the REPL start once the GitHub integration has been removed
# (e.g. via ``/integrations remove github``). Re-checking the store is cheap,
# so the gate always re-runs when GitHub is not currently configured.
return not _github_already_configured()
def clear_github_login_deferral() -> None:
"""Clear a saved skip so removing GitHub can re-prompt on the next launch."""
if not read_github_login_deferred():
return
write_github_login_deferred(False)
def _propagate_username(username: str) -> None:
if not username:
return
# ``authenticate_and_configure_github`` already calls identify_github_username;
# only emit the one-time login lifecycle event here.
capture_github_login_completed(username)
def _print_intro(console: Console) -> None:
console.print()
console.print("[bold]Connect GitHub to get started[/bold]")
console.print(
"OpenSRE needs read access to your GitHub repositories to investigate "
"incidents against your source. Sign in once with your browser."
)
console.print(
"[dim](Escape to skip for now, or set "
f"{_SKIP_ENV_VAR}=1 if GitHub sign-in is unavailable.)[/dim]"
)
def _show_device_code(console: Console, code: object) -> None:
from integrations.github.mcp_oauth import GitHubDeviceCode
if not isinstance(code, GitHubDeviceCode):
return
user_code = escape(code.user_code)
console.print()
console.print(f" 1. Your browser will open [underline]{code.verification_uri}[/underline]")
console.print(" (if it doesn't open automatically, visit that URL yourself).")
console.print(f" 2. Enter this one-time code when GitHub asks: [{DEVICE_CODE}]{user_code}[/]")
console.print(" 3. Approve the request for OpenSRE.")
console.print()
console.print(
" [dim]Waiting for you to approve in the browser… (Escape or Ctrl-C to skip)[/dim]"
)
def _print_skip_guidance(console: Console) -> None:
console.print()
console.print(
"[dim]Skipped GitHub sign-in. Connect later with "
"[bold]/integrations setup[/bold] or [bold]/mcp connect github[/bold].[/dim]"
)
def _defer_github_login() -> None:
write_github_login_deferred(True)
def _sleep_until_or_cancel(seconds: float) -> None:
"""Sleep up to ``seconds``, raising ``KeyboardInterrupt`` when the user skips."""
if seconds <= 0 or not sys.stdin.isatty():
time.sleep(seconds)
return
if os.name == "nt":
import msvcrt
from surfaces.interactive_shell.ui.components.key_reader import read_key_windows
deadline = time.monotonic() + seconds
while time.monotonic() < deadline:
if msvcrt.kbhit() and read_key_windows() == "cancel": # type: ignore[attr-defined]
raise KeyboardInterrupt
time.sleep(0.05)
return
import select
import termios
import tty
from surfaces.interactive_shell.ui.components.key_reader import read_key_unix
fd = sys.stdin.fileno()
old_attrs = termios.tcgetattr(fd) # type: ignore[attr-defined]
try:
tty.setraw(fd) # type: ignore[attr-defined]
deadline = time.monotonic() + seconds
while time.monotonic() < deadline:
remaining = deadline - time.monotonic()
if remaining <= 0:
return
ready, _, _ = select.select([fd], [], [], min(remaining, 0.15))
if not ready:
continue
if read_key_unix() == "cancel":
raise KeyboardInterrupt
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_attrs) # type: ignore[attr-defined]
def _offer_github_login(_console: Console) -> bool:
"""Return True when the user wants to start browser sign-in."""
import questionary
try:
choice = questionary.select(
"Connect GitHub now?",
choices=[
questionary.Choice(
"Sign in with GitHub (opens browser)",
value=_SIGN_IN_CHOICE,
),
questionary.Choice("Skip for now", value=_SKIP_CHOICE),
],
default=_SIGN_IN_CHOICE,
).ask()
except (EOFError, KeyboardInterrupt):
return False
if choice is None:
return False
return bool(choice == _SIGN_IN_CHOICE)
def _ask_retry(_console: Console) -> bool:
import questionary
try:
answer = questionary.confirm("Try GitHub sign-in again?", default=True).ask()
except (EOFError, KeyboardInterrupt):
return False
if answer is None:
return False
return bool(answer)
def _attempt_login(console: Console) -> str:
"""Run one login attempt. Returns ``"success"``, ``"failed"``, or ``"skipped"``."""
from integrations.github.login import authenticate_and_configure_github
from integrations.github.mcp_oauth import GitHubDeviceFlowError
try:
result = authenticate_and_configure_github(
on_prompt=lambda code: _show_device_code(console, code),
poll_sleep=_sleep_until_or_cancel,
)
except (EOFError, KeyboardInterrupt):
console.print("\nSkipped GitHub sign-in.")
return "skipped"
except GitHubDeviceFlowError as err:
console.print(f"[yellow]GitHub sign-in is unavailable:[/yellow] {err}")
return "failed"
except Exception as err: # network/transport issues
console.print(f"[yellow]GitHub sign-in failed:[/yellow] {err}")
return "failed"
if result.ok:
clear_github_login_deferral()
# Persisting the GitHub integration (done inside
# ``authenticate_and_configure_github``) is what suppresses the gate on
# subsequent launches — there is no separate completion marker to write.
_propagate_username(result.username)
who = f"@{result.username}" if result.username else "your GitHub account"
console.print(f"[bold]Connected.[/bold] Signed in as {who}.")
return "success"
console.print(f"[yellow]Could not verify GitHub access:[/yellow] {result.detail}")
return "failed"
def require_github_login_on_first_launch(console: Console | None = None) -> bool:
"""Run the first-launch GitHub login prompt.
Returns True when the caller should proceed into the REPL (login succeeded or
the user skipped), and False only when startup must abort.
"""
con = console or Console(highlight=False)
_print_intro(con)
if not _offer_github_login(con):
_defer_github_login()
_print_skip_guidance(con)
return True
while True:
outcome = _attempt_login(con)
if outcome == "success":
return True
if outcome == "skipped":
_defer_github_login()
_print_skip_guidance(con)
return True
if not _ask_retry(con):
_defer_github_login()
_print_skip_guidance(con)
return True
def require_startup_github_login(console: Console) -> bool:
"""Return True when startup may proceed past the GitHub login gate.
On an unexpected gate error we deliberately do NOT fail open into the REPL:
that would let a gate bug silently skip sign-in. Instead we only allow
startup when an explicit, documented bypass applies.
"""
try:
if not should_require_github_login():
return True
return require_github_login_on_first_launch(console)
except Exception:
logging.getLogger(__name__).warning(
"First-launch GitHub login gate failed.",
exc_info=True,
)
if _github_login_explicitly_bypassed():
return True
console.print(
"GitHub sign-in could not run. "
f"Set [bold]{_SKIP_ENV_VAR}=1[/bold] to bypass this, then relaunch "
"[bold]opensre[/bold]."
)
return False