Files
wehub-resource-sync 0ef5fcb1c5
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:03:20 +08:00

248 lines
8.8 KiB
Python

"""Download and install rtk binary from GitHub releases."""
from __future__ import annotations
import io
import logging
import os
import platform
import stat
import subprocess
import tarfile
import tempfile
import zipfile
from pathlib import Path
from urllib.request import urlopen
from headroom._subprocess import run
from . import RTK_BIN_DIR, RTK_BIN_PATH, RTK_VERSION
logger = logging.getLogger(__name__)
GITHUB_RELEASE_URL = "https://github.com/rtk-ai/rtk/releases/download"
def _detect_runtime_target_triple() -> str:
"""Detect platform and return the rtk release target triple."""
system = platform.system()
machine = platform.machine()
if system == "Darwin":
arch = "aarch64" if machine == "arm64" else "x86_64"
return f"{arch}-apple-darwin"
elif system == "Linux":
arch = "aarch64" if machine == "aarch64" else "x86_64"
suffix = "unknown-linux-gnu" if arch == "aarch64" else "unknown-linux-musl"
return f"{arch}-{suffix}"
elif system == "Windows":
return "x86_64-pc-windows-msvc"
raise RuntimeError(f"Unsupported platform: {system} {machine}")
def _get_target_triple() -> str:
"""Return the requested rtk target triple, honoring explicit overrides."""
return os.environ.get("HEADROOM_RTK_TARGET", "").strip() or _detect_runtime_target_triple()
def _binary_name_for_target(target: str) -> str:
"""Return the expected binary name for a target triple."""
return "rtk.exe" if "windows" in target else "rtk"
def _should_verify_target(target: str) -> bool:
"""Verify only when the requested target matches the current runtime."""
return target == _detect_runtime_target_triple()
def _get_download_url(version: str) -> tuple[str, str]:
"""Get download URL and extension for this platform.
Returns (url, extension) where extension is 'tar.gz' or 'zip'.
"""
target = _get_target_triple()
if "windows" in target:
ext = "zip"
else:
ext = "tar.gz"
url = f"{GITHUB_RELEASE_URL}/{version}/rtk-{target}.{ext}"
return url, ext
def download_rtk(version: str | None = None) -> Path:
"""Download rtk binary from GitHub releases.
Args:
version: Version to download (e.g., "v0.42.4"). Defaults to pinned version.
Returns:
Path to the installed binary.
Raises:
RuntimeError: If download or extraction fails.
"""
version = version or RTK_VERSION
target = _get_target_triple()
url, ext = _get_download_url(version)
target_path = RTK_BIN_DIR / _binary_name_for_target(target)
RTK_BIN_DIR.mkdir(parents=True, exist_ok=True)
logger.info("Downloading rtk %s from %s ...", version, url)
try:
# Validate URL scheme to prevent B310 warning
if not url.startswith(("http://", "https://")):
raise ValueError(f"Invalid URL scheme in {url}")
# Fail closed on TLS errors rather than executing an unverifiable download.
try:
with urlopen(url, timeout=30) as response:
data = response.read()
except Exception as download_err:
if "CERTIFICATE_VERIFY_FAILED" in str(download_err):
raise RuntimeError(
"TLS verification failed downloading rtk; fix the local trust store and retry."
) from download_err
raise
except Exception as e:
raise RuntimeError(f"Failed to download rtk from {url}: {e}") from e
# Extract binary
try:
if ext == "tar.gz":
with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as tar:
# Find the rtk binary inside the archive
for member in tar.getmembers():
if member.name.endswith("/rtk") or member.name == "rtk":
member.name = target_path.name # Flatten path
tar.extract(member, RTK_BIN_DIR)
break
else:
raise RuntimeError("rtk binary not found in archive")
elif ext == "zip":
with zipfile.ZipFile(io.BytesIO(data)) as zf:
for name in zf.namelist():
if name.endswith("rtk.exe") or name.endswith("/rtk"):
with zf.open(name) as src, open(target_path, "wb") as dst:
dst.write(src.read())
break
else:
raise RuntimeError("rtk binary not found in archive")
except (tarfile.TarError, zipfile.BadZipFile) as e:
raise RuntimeError(f"Failed to extract rtk archive: {e}") from e
# Make executable (skip on Windows — no Unix permissions)
if "windows" not in target:
target_path.chmod(target_path.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
if _should_verify_target(target):
try:
result = run(
[str(target_path), "--version"],
capture_output=True,
text=True,
timeout=5,
)
if result.returncode != 0:
raise RuntimeError(f"rtk verification failed: {result.stderr}")
logger.info("rtk installed: %s", result.stdout.strip())
except FileNotFoundError as e:
raise RuntimeError("rtk binary not found after extraction") from e
except subprocess.TimeoutExpired as e:
raise RuntimeError("rtk verification timed out") from e
else:
logger.info("rtk installed for target %s at %s (verification skipped)", target, target_path)
return target_path
# Agents rtk registers a *native* hook for via `rtk init --agent <name>`.
# For these, headroom must not also inject the RTK_INSTRUCTIONS_BLOCK text
# into a rules/instructions file — that duplicates guidance rtk's own hook
# already provides silently (GH #756).
RTK_NATIVE_HOOK_AGENTS = frozenset(
{"claude", "cursor", "windsurf", "cline", "kilocode", "antigravity", "pi", "hermes"}
)
def register_claude_hooks(rtk_path: Path | None = None) -> bool:
"""Register rtk hooks in Claude Code settings.
Runs `rtk init --global` which adds a PreToolUse hook to
~/.claude/settings.json that rewrites Bash commands through rtk.
Returns True if hooks were registered successfully.
"""
return register_agent_hooks(rtk_path, agent="claude")
def register_agent_hooks(rtk_path: Path | None = None, *, agent: str = "claude") -> bool:
"""Register rtk's native hook for ``agent`` via ``rtk init --agent``.
Only agents in ``RTK_NATIVE_HOOK_AGENTS`` support this; callers must not
invoke this for agents rtk has no native hook for (rtk itself will just
reject the ``--agent`` value).
Returns True if hooks were registered successfully.
"""
rtk_path = rtk_path or RTK_BIN_PATH
args = [str(rtk_path), "init", "--global", "--auto-patch"]
if agent != "claude":
args += ["--agent", agent]
# Capture output to a temp file rather than pipes: `rtk init` may fork a
# background process that inherits our stdout/stderr, and a piped
# `subprocess.run` drains those pipes until EOF — which never arrives while
# the daemon holds them open, so it blocks to the timeout even though
# `rtk init` itself exited and already registered the hooks. A file fd has
# no such reader, so we wait only on the direct child. stdin is DEVNULL so a
# stray prompt can never block either.
try:
with tempfile.TemporaryFile(mode="w+", encoding="utf-8", errors="replace") as out:
try:
result = subprocess.run(
args,
stdin=subprocess.DEVNULL,
stdout=out,
stderr=out,
timeout=10,
)
except subprocess.TimeoutExpired:
# Read the temp file while it is still open — the outer handler
# runs after the `with` closes it, so any captured diagnostics
# would be gone by then.
out.seek(0)
logger.warning("rtk init timed out: %s", out.read().strip())
return False
if result.returncode == 0:
logger.info("rtk hooks registered for %s", agent)
return True
out.seek(0)
logger.warning("rtk init failed: %s", out.read().strip())
return False
except Exception as e:
logger.warning("Failed to register rtk hooks: %s", e)
return False
def ensure_rtk(version: str | None = None) -> Path | None:
"""Ensure rtk is installed — download if needed.
Returns path to rtk binary, or None if installation failed.
"""
from . import get_rtk_path
existing = get_rtk_path()
if existing:
return existing
try:
return download_rtk(version)
except RuntimeError as e:
logger.warning("Could not install rtk: %s", e)
return None