"""Read-only service for the CLI distribution manifest + download URLs. The release pipeline (``.github/workflows/cli-release.yml``) uploads, on every ``main`` build, to the storage bucket under a FIXED top-level namespace ``cli-dist/`` (deliberately NOT the per-env ``dev/`` upload prefix — a CLI binary is identical across environments, there's nothing to isolate): cli-dist//helix- # PyInstaller binaries cli-dist//helix_cli--py3-none-any.whl cli-dist/latest/manifest.json # pointer + checksums This service reads ``latest/manifest.json`` server-side and, for a requested platform, mints a presigned GET. Everything degrades to "unavailable" (``None``) rather than 500 when the manifest is missing (e.g. before the first release, or in a local dev env with no S3 creds) — the UI then simply hides the download affordance. """ from __future__ import annotations import json import logging from typing import Final from sqlalchemy.ext.asyncio import AsyncSession from app.core.storage import ( StorageNotConfiguredError, get_object_bytes, presign_get, ) logger = logging.getLogger(__name__) # Fixed manifest location (absolute key — no env prefix; see module doc). MANIFEST_KEY: Final[str] = "cli-dist/latest/manifest.json" # The Claude Code plugin release (``plugin-release.yml``) publishes the # same way under a sibling namespace: a single platform-independent # marketplace zip + a ``latest`` manifest pointer. PLUGIN_MANIFEST_KEY: Final[str] = "plugin-dist/latest/manifest.json" # The plugin ships exactly one artifact shape. Same allowlist role as # PLATFORM_LABELS: anything else in the manifest is ignored, and a # download request for another key is rejected up front. PLUGIN_PLATFORM_LABELS: Final[dict[str, str]] = { "any": "Claude Code marketplace zip (all platforms)", } # Platform allowlist → human label. A download request for a platform # not in this map is rejected before we ever touch the manifest, and a # manifest artifact whose platform isn't here is ignored. Keep in sync # with the build matrix in cli-release.yml. PLATFORM_LABELS: Final[dict[str, str]] = { "macos-arm64": "macOS (Apple Silicon)", "macos-x86_64": "macOS (Intel)", "linux-x86_64": "Linux (x86_64)", "linux-arm64": "Linux (arm64)", "windows-x86_64": "Windows (x86_64)", "wheel": "Python wheel (pip / pipx)", } # Download-URL lifetime. Short — the browser follows the redirect # immediately; a longer window only widens the leak surface if the # 302 Location is captured. _DOWNLOAD_TTL_SECONDS: Final[int] = 300 def _key_in_namespace(key: str, namespace: str) -> bool: """Presign only keys inside the release namespace the manifest belongs to. The manifest lives in the shared uploads bucket; without this, whoever can write the manifest could point an ``key`` at ANY bucket object and have an employee's authenticated download call mint a presigned GET for it (closeout hardening, 2026-07-09 — latent capability, not exploitable without bucket write, closed anyway).""" if not isinstance(key, str) or not key.startswith(namespace): logger.warning("cli_dist: manifest key %r outside %s — refused", key, namespace) return False # No traversal-ish segments even though S3 keys are flat strings — # presign_get should never see a normalizable path. return ".." not in key async def _load_manifest( db: AsyncSession, *, manifest_key: str = MANIFEST_KEY ) -> dict | None: """Fetch + parse a ``latest/manifest.json``; ``None`` if it is absent / unconfigured / malformed (callers render "unavailable").""" try: raw = await get_object_bytes(db, object_key=manifest_key) except StorageNotConfiguredError: return None except Exception: # noqa: BLE001 — never 500 a download page on S3 hiccup logger.warning( "cli_dist: failed to read manifest %s from S3", manifest_key, exc_info=True ) return None if raw is None: return None try: data = json.loads(raw.decode("utf-8")) except (ValueError, UnicodeDecodeError): logger.warning("cli_dist: manifest %s is not valid JSON", manifest_key) return None if not isinstance(data, dict) or not isinstance(data.get("artifacts"), list): logger.warning("cli_dist: manifest %s has unexpected shape", manifest_key) return None return data def _public_artifacts( manifest: dict, *, labels: dict[str, str] = PLATFORM_LABELS ) -> list[dict]: """Project manifest artifacts to the public (no S3 key) shape, in a stable display order, dropping any unknown platform.""" by_platform = { a.get("platform"): a for a in manifest["artifacts"] if isinstance(a, dict) and a.get("platform") in labels } out: list[dict] = [] for platform in labels: # stable, allowlist-driven order a = by_platform.get(platform) if not a: continue out.append( { "platform": platform, "label": labels[platform], "kind": a.get("kind"), "filename": a.get("filename"), "sha256": a.get("sha256"), "size": a.get("size"), } ) return out async def get_manifest(db: AsyncSession) -> dict: """Public manifest for the UI / CLI self-check: latest version + minimum supported version + per-platform metadata (incl. sha256 for integrity verification), never the raw S3 keys. ``available=false`` when no release is published. ``min_supported`` is the server's floor (settings.CLI_MIN_SUPPORTED_VERSION): a CLI below it is refused at the MCP edge, so ``helix version`` can warn the user to upgrade BEFORE they hit a hard 426. """ from app.core.config import settings min_supported = settings.CLI_MIN_SUPPORTED_VERSION manifest = await _load_manifest(db) if manifest is None: return {"available": False, "min_supported": min_supported, "platforms": []} return { "available": True, "version": manifest.get("version"), "min_supported": min_supported, "generated_at": manifest.get("generated_at"), "commit": manifest.get("commit"), "platforms": _public_artifacts(manifest), } async def presign_download(db: AsyncSession, *, platform: str) -> dict | None: """Resolve ``platform`` → presigned GET URL + metadata, or ``None`` if the platform is unknown / not present in the current manifest. Caller MUST have validated the Helix session already — the returned URL bypasses Helix auth once minted (same posture as ``storage.presign_get``). """ if platform not in PLATFORM_LABELS: return None manifest = await _load_manifest(db) if manifest is None: return None artifact = next( ( a for a in manifest["artifacts"] if isinstance(a, dict) and a.get("platform") == platform and a.get("key") ), None, ) if artifact is None: return None if not _key_in_namespace(artifact["key"], "cli-dist/"): return None url = await presign_get( db, object_key=artifact["key"], expires_in=_DOWNLOAD_TTL_SECONDS ) return { "url": url, "platform": platform, "filename": artifact.get("filename"), "sha256": artifact.get("sha256"), "size": artifact.get("size"), "version": manifest.get("version"), "expires_in": _DOWNLOAD_TTL_SECONDS, } # ── Claude Code plugin distribution (plugin-dist/) ─────────────────── # # Same manifest + presign mechanics as the CLI above; kept in this # domain deliberately — "self-service artifact distribution" is one # capability, not two (ADR: no extra domain for a second S3 namespace). async def get_plugin_manifest(db: AsyncSession) -> dict: """Public manifest for the Claude Code plugin download card and the ``plugin_get_download_manifest`` MCP tool. ``available=false`` when no release is published / storage unconfigured — never a 500.""" manifest = await _load_manifest(db, manifest_key=PLUGIN_MANIFEST_KEY) if manifest is None: return {"available": False, "platforms": []} return { "available": True, "version": manifest.get("version"), # Human-facing plugin.json version (distribution version above # carries the +g traceability suffix). "plugin_version": manifest.get("plugin_version"), "generated_at": manifest.get("generated_at"), "commit": manifest.get("commit"), "platforms": _public_artifacts(manifest, labels=PLUGIN_PLATFORM_LABELS), } async def presign_plugin_download(db: AsyncSession) -> dict | None: """Presigned GET for the marketplace zip (single ``any`` artifact), or ``None`` when unpublished. Same auth posture as the CLI download: caller must hold a valid Helix session; the artifact itself is the same for every employee.""" manifest = await _load_manifest(db, manifest_key=PLUGIN_MANIFEST_KEY) if manifest is None: return None artifact = next( ( a for a in manifest["artifacts"] if isinstance(a, dict) and a.get("platform") in PLUGIN_PLATFORM_LABELS and a.get("key") ), None, ) if artifact is None: return None if not _key_in_namespace(artifact["key"], "plugin-dist/"): return None url = await presign_get( db, object_key=artifact["key"], expires_in=_DOWNLOAD_TTL_SECONDS ) return { "url": url, "platform": artifact.get("platform"), "filename": artifact.get("filename"), "sha256": artifact.get("sha256"), "size": artifact.get("size"), "version": manifest.get("version"), "plugin_version": manifest.get("plugin_version"), "expires_in": _DOWNLOAD_TTL_SECONDS, }