- Introduced `SessionRename` model for renaming sessions with validation on name length and content. - Implemented `rename_session` endpoint in `session_routes.py` to handle session renaming. - Added `delete_session` endpoint to remove sessions, ensuring proper cleanup of associated data. - Enhanced `SessionStore` with `rename` and `delete` methods, including validation and transaction management. - Updated `SessionManager` to manage session renaming and deletion, ensuring live terminal state is preserved. - Added tests for renaming and deleting sessions, ensuring proper functionality and validation. - Introduced a new `tool_catalog.py` for managing guest tool selections and installation recipes.
163 lines
9.2 KiB
Python
163 lines
9.2 KiB
Python
"""The allowlisted guest tool catalog and pinned, architecture-specific recipes.
|
|
|
|
Download digests come from the publishers' release SHA256SUMS/.sha256 files:
|
|
Node v24.21.0, python-build-standalone 20260924, Miniforge 26.5.3-0, and
|
|
micromamba-releases 2.9.0-0. Update versions, URLs, and digests together.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from .errors import ValidationError
|
|
|
|
|
|
CATALOG = (
|
|
{"id": "git", "name": "Git", "description": "Version control for your projects.",
|
|
"version_label": "Ubuntu package", "versions": [], "source": "Ubuntu"},
|
|
{"id": "nodejs", "name": "Node.js", "description": "JavaScript runtime with npm and npx.",
|
|
"version_label": "24 LTS · 24.21.0", "versions": [], "source": "nodejs.org"},
|
|
{"id": "python", "name": "Python", "description": "Side-by-side Python runtimes with pip and venv.",
|
|
"version_label": "3.11 / 3.12 / 3.13", "versions": ["3.11", "3.12", "3.13"],
|
|
"source": "Astral python-build-standalone"},
|
|
{"id": "jdk", "name": "JDK", "description": "OpenJDK development kit: java and javac.",
|
|
"version_label": "OpenJDK 21 LTS", "versions": [], "source": "Ubuntu"},
|
|
{"id": "wget", "name": "wget", "description": "Download files from the command line.",
|
|
"version_label": "Ubuntu package", "versions": [], "source": "Ubuntu"},
|
|
{"id": "curl", "name": "curl", "description": "Transfer data and call HTTP APIs.",
|
|
"version_label": "Ubuntu package", "versions": [], "source": "Ubuntu"},
|
|
{"id": "htop", "name": "htop", "description": "Interactive process and resource monitor.",
|
|
"version_label": "Ubuntu package", "versions": [], "source": "Ubuntu"},
|
|
{"id": "php", "name": "PHP", "description": "PHP command-line runtime.",
|
|
"version_label": "8.5 CLI", "versions": [], "source": "Ondřej Surý PHP PPA"},
|
|
{"id": "conda", "name": "Conda", "description": "Miniforge with the conda-forge package channel.",
|
|
"version_label": "Miniforge 26.5.3-0", "versions": [], "source": "conda-forge"},
|
|
{"id": "micromamba", "name": "micromamba", "description": "Lightweight Conda-compatible environment manager.",
|
|
"version_label": "2.9.0", "versions": [], "source": "mamba-org"},
|
|
)
|
|
TOOL_IDS = frozenset(tool["id"] for tool in CATALOG)
|
|
MAX_SELECTIONS = 12
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ToolSelection:
|
|
id: str
|
|
version: str | None = None
|
|
|
|
def __post_init__(self) -> None:
|
|
if not isinstance(self.id, str) or self.id not in TOOL_IDS:
|
|
raise ValidationError("unknown tool; choose a tool from GET /tools")
|
|
if self.id == "python":
|
|
if self.version not in ("3.11", "3.12", "3.13"):
|
|
raise ValidationError("Python requires version 3.11, 3.12, or 3.13")
|
|
elif self.version is not None:
|
|
raise ValidationError("only Python accepts a version; other tools use the catalog default")
|
|
|
|
@property
|
|
def key(self) -> str:
|
|
return f"{self.id}-{self.version}" if self.version else self.id
|
|
|
|
def to_dict(self) -> dict:
|
|
return {"id": self.id, "version": self.version}
|
|
|
|
|
|
def normalize_tools(values: object, *, allow_empty: bool = False) -> tuple[ToolSelection, ...]:
|
|
if not isinstance(values, (list, tuple)) or len(values) > MAX_SELECTIONS:
|
|
raise ValidationError(f"tools must be a list of at most {MAX_SELECTIONS} catalog selections")
|
|
result = {}
|
|
for value in values:
|
|
if isinstance(value, ToolSelection):
|
|
selection = ToolSelection(value.id, value.version)
|
|
elif isinstance(value, dict) and "id" in value and not set(value) - {"id", "version"}:
|
|
selection = ToolSelection(value["id"], value.get("version"))
|
|
else:
|
|
raise ValidationError("each tool selection requires an id and optional Python version")
|
|
result[selection.key] = selection
|
|
if not result and not allow_empty:
|
|
raise ValidationError("select at least one tool to install")
|
|
# Stable ordering also makes duplicate requests independent of checkbox order.
|
|
order = {tool["id"]: index for index, tool in enumerate(CATALOG)}
|
|
return tuple(sorted(result.values(), key=lambda item: (order[item.id], item.version or "")))
|
|
|
|
|
|
def all_selections() -> tuple[ToolSelection, ...]:
|
|
return tuple(ToolSelection(tool["id"], version) for tool in CATALOG for version in tool["versions"] or [None])
|
|
|
|
|
|
_PYTHON_RELEASE = "20260924"
|
|
_PYTHON = {
|
|
"3.11": ("3.11.16", {
|
|
"x86_64": "68c6739376b65258dee5058ccf6777232fe38d31a578965ae8bda327ec7da3a8",
|
|
"aarch64": "763dae20c6abe982de86ceb7829cc050c5bda79a9824846d106cc1c86d57701b",
|
|
}),
|
|
"3.12": ("3.12.14", {
|
|
"x86_64": "269b2c99e4db15b242bf01832f4fea1e8f1a664f273cff519393f296e9820b41",
|
|
"aarch64": "c8499b61252c433280f134df954464d19811527b31cb920c35fc6967c1222e35",
|
|
}),
|
|
"3.13": ("3.13.15", {
|
|
"x86_64": "d0b640eed27fbdd6f5f2bd33444aee53df2c8863f8b2a96f4094717411e3de9c",
|
|
"aarch64": "5ad58156cbec94e5643c13caa792e92df72a23f33d3a6425d4cbff5c4b7a040c",
|
|
}),
|
|
}
|
|
_NODE_SHA256 = {
|
|
"x86_64": "fd8e59d5a511510f6a298afb548f18c7d2b1be404d8b4a27d94fbe49f56cb2d6",
|
|
"aarch64": "6ad1325edbdb5649c379b75a237147a666c95d4f9ae8d340fef2d1575d289ad2",
|
|
}
|
|
_CONDA_SHA256 = {
|
|
"x86_64": "14db468222ad564658656f769506056209b6dc375f5e7dfd31eb5ebbf08fa529",
|
|
"aarch64": "0391e42075a7632e9665d6e728387ee6b905f6c3e704d3513e1c1133d0d69b89",
|
|
}
|
|
_MICROMAMBA_SHA256 = {
|
|
"x86_64": "366cd9cd8be14df1ab8ed50352a82111082a36686b2d389fdb79a92c3fafb3e3",
|
|
"aarch64": "9f93b974adcb4d166996af969b6cd371287d1a3e52733704727884d9b74cb7a7",
|
|
}
|
|
|
|
|
|
def recipe(selection: ToolSelection, architecture: str) -> dict:
|
|
"""Return a JSON-serializable recipe; no request text becomes a shell program."""
|
|
if architecture not in {"x86_64", "aarch64"}:
|
|
raise ValidationError("tool installation supports x86_64 and ARM64 guests")
|
|
tool = selection.id
|
|
result = {**selection.to_dict(), "key": selection.key, "kind": "apt", "packages": [tool],
|
|
"checks": [[tool, "--version"]], "match": r"(\d+(?:\.\d+)+)", "prefix_match": "",
|
|
"links": {}, "usage": f"{tool} --version"}
|
|
if tool == "nodejs":
|
|
arch = "x64" if architecture == "x86_64" else "arm64"
|
|
result.update(kind="archive", release="nodejs-24.21.0", packages=["ca-certificates", "libstdc++6"],
|
|
url=f"https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-{arch}.tar.xz",
|
|
sha256=_NODE_SHA256[architecture], archive_root=f"node-v24.21.0-linux-{arch}",
|
|
checks=[["node", "--version"], ["npm", "--version"], ["npx", "--version"]], prefix_match="24.",
|
|
links={name: f"bin/{name}" for name in ("node", "npm", "npx")}, usage="node --version; npm --version")
|
|
elif tool == "python":
|
|
version, hashes = _PYTHON[selection.version]
|
|
binary = f"python{selection.version}"
|
|
result.update(kind="archive", release=f"python-{version}", packages=["ca-certificates", "libcrypt1"],
|
|
url=(f"https://github.com/astral-sh/python-build-standalone/releases/download/{_PYTHON_RELEASE}/"
|
|
f"cpython-{version}+{_PYTHON_RELEASE}-{architecture}-unknown-linux-gnu-install_only_stripped.tar.gz"),
|
|
sha256=hashes[architecture], archive_root="python", prefix_match=selection.version + ".",
|
|
checks=[[binary, "-c", "import sys, ssl, sqlite3, venv, ensurepip; print(sys.version.split()[0])"],
|
|
[binary, "-m", "pip", "--version"]], links={binary: f"bin/{binary}"},
|
|
usage=f"{binary} -m venv .venv; . .venv/bin/activate")
|
|
elif tool == "jdk":
|
|
arch = "amd64" if architecture == "x86_64" else "arm64"
|
|
home = f"/usr/lib/jvm/java-21-openjdk-{arch}"
|
|
result.update(packages=["openjdk-21-jdk-headless"], checks=[["javac", "-version"], ["java", "-version"]],
|
|
prefix_match="21.", java_home=home, links={name: f"{home}/bin/{name}" for name in ("java", "javac")},
|
|
usage="java -version; javac -version")
|
|
elif tool == "php":
|
|
result.update(kind="php", packages=["php8.5-cli"], prefix_match="8.5.",
|
|
links={"php": "/usr/bin/php8.5"}, usage="php --version")
|
|
elif tool == "conda":
|
|
result.update(kind="conda", release="miniforge-26.5.3-0", packages=["ca-certificates", "bzip2"],
|
|
url=("https://github.com/conda-forge/miniforge/releases/download/26.5.3-0/"
|
|
f"Miniforge3-26.5.3-0-Linux-{architecture}.sh"), sha256=_CONDA_SHA256[architecture],
|
|
links={"conda": "bin/conda", "mamba": "bin/mamba"},
|
|
usage="conda create -n myenv python=3.13; conda activate myenv")
|
|
elif tool == "micromamba":
|
|
arch = "64" if architecture == "x86_64" else "aarch64"
|
|
result.update(kind="binary", release="micromamba-2.9.0", packages=["ca-certificates"],
|
|
url=f"https://github.com/mamba-org/micromamba-releases/releases/download/2.9.0-0/micromamba-linux-{arch}",
|
|
sha256=_MICROMAMBA_SHA256[architecture], links={"micromamba": "micromamba"},
|
|
usage="micromamba create -n myenv -c conda-forge python=3.13; micromamba activate myenv")
|
|
return result
|