feat(python): pin bundled runtime to 3.14 everywhere (pm, termux lane, CI, installers)

pm python node: 3.14.7+20260901 (freshest python-build-standalone 3.14
build) for the 6 desktop targets; the bionic row moves from the third-party
TUR python3.11 deb to the official termux-main python_3.14.6-1 deb (which
lags PBS by one patch — pinned manually, documented). All 7 digests fetched
from the live sources (PBS release API + termux-main Packages index).
pm/packages.py: main_bin_rel python3.14, deb_package python, bionic fetch
constant, latest_versions guards bionic (no PBS build exists).

termux lane: PYTHON_ABI cp311->cp314, python3.11->python3.14 paths,
libpython3.11.so->3.14, TARGET_ENV 3.11.15->3.14.6 AND sys_platform
linux->android (CPython 3.13+ reports 'android', docs-verified) — linux-
gated markers no longer admit the termux target. runtime_libs.json needs no
change: every python 3.14.6-1 Depends is already staged.

CI: python-version/--python 3.11->3.14 across all 11 workflows incl. the
uv lockfile-check lane. Installers derive the minor from the lock already;
fallbacks bumped. Sandbox images nikolaik/python-nodejs:python3.11-nodejs20
-> python3.14-nodejs22 (tag exists). runtime_repair fall-forward cap now
tracks the <3.15 requires-python window. Docs/README python version claims
updated.
This commit is contained in:
ethernet
2026-09-07 14:19:18 -04:00
parent 7f1ddc70ce
commit cd0f97f833
38 changed files with 134 additions and 122 deletions
+1 -1
View File
@@ -75,7 +75,7 @@ jobs:
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.11'
python-version: '3.14'
- name: Install PyYAML for skill extraction
uses: ./.github/actions/retry
@@ -430,12 +430,12 @@ jobs:
AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }}
AZURE_TOKEN_CREDENTIALS: prod
run: |
uv run --no-project --python 3.11 python scripts/bundles/desktop.py --tag="$HERMES_PAYLOAD_TAG" --variant=bundled
uv run --no-project --python 3.14 python scripts/bundles/desktop.py --tag="$HERMES_PAYLOAD_TAG" --variant=bundled
# The Store-submission MSIX is the same bundled payload re-packed
# with the Partner Center packaging identity (publish-win32-store
# bundles these into the universal Store .msixbundle and submits it;
# they also land in the tag archive, never a feed dir).
uv run --no-project --python 3.11 python scripts/bundles/desktop.py --tag="$HERMES_PAYLOAD_TAG" --variant=store
uv run --no-project --python 3.14 python scripts/bundles/desktop.py --tag="$HERMES_PAYLOAD_TAG" --variant=store
- name: Verify native signature cache contracts
shell: bash
@@ -730,7 +730,7 @@ jobs:
# every Mach-O) — raise the fd limit and let DEBUG show progress.
ulimit -n 16384 2>/dev/null || true
echo "file descriptor limit: soft=$(ulimit -Sn) hard=$(ulimit -Hn)"
uv run --no-project --python 3.11 python scripts/bundles/desktop.py --tag="$HERMES_PAYLOAD_TAG" --variant=bundled
uv run --no-project --python 3.14 python scripts/bundles/desktop.py --tag="$HERMES_PAYLOAD_TAG" --variant=bundled
- name: Audit bundle architecture
shell: bash
+3 -3
View File
@@ -150,10 +150,10 @@ jobs:
# fail the job (2026-07-28 incident). Keep in sync with tests.yml.
version: "0.9.28"
- name: Set up Python 3.11 (for docker tests)
- name: Set up Python 3.14 (for docker tests)
uses: ./.github/actions/retry
with:
command: uv python install 3.11
command: uv python install 3.14
- name: Install Python dependencies (for docker tests)
# ``dev`` extra pulls in pytest, pytest-asyncio —
@@ -162,7 +162,7 @@ jobs:
# subprocess and don't import hermes_agent's optional deps.
uses: ./.github/actions/retry
with:
command: uv sync --locked --python 3.11 --extra dev
command: uv sync --locked --python 3.14 --extra dev
- name: Run docker integration tests
env:
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
python-version: "3.14"
- name: Install ascii-guard
uses: ./.github/actions/retry
+3 -3
View File
@@ -70,15 +70,15 @@ jobs:
pyproject.toml
uv.lock
- name: Set up Python 3.11
- name: Set up Python 3.14
uses: ./.github/actions/retry
with:
command: uv python install 3.11
command: uv python install 3.14
- name: Install Python dependencies
uses: ./.github/actions/retry
with:
command: uv sync --locked --python 3.11 --extra all --extra dev
command: uv sync --locked --python 3.14 --extra all --extra dev
# ── Build desktop app ─────────────────────────────────────────────
# The Playwright step below runs `npm run build` before testing so
+2 -2
View File
@@ -171,10 +171,10 @@ jobs:
# fail the job (2026-07-28 incident). Keep in sync with tests.yml.
version: "0.9.28"
- name: Set up Python 3.11
- name: Set up Python 3.14
uses: ./.github/actions/retry
with:
command: uv python install 3.11
command: uv python install 3.14
- name: Run footgun checker
run: python scripts/check-windows-footguns.py --all
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
python-version: "3.14"
- name: Install dependencies
uses: ./.github/actions/retry
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y patchelf
uv venv --python 3.11 .venv
uv venv --python 3.14 .venv
uv export --frozen --extra dev --no-emit-project --no-hashes -o "$RUNNER_TEMP/requirements.txt"
uv pip install --python .venv/bin/python -r "$RUNNER_TEMP/requirements.txt"
- name: Run the native linker and wheel contracts
+3 -3
View File
@@ -80,10 +80,10 @@ jobs:
pyproject.toml
uv.lock
- name: Set up Python 3.11
- name: Set up Python 3.14
uses: ./.github/actions/retry
with:
command: uv python install 3.11
command: uv python install 3.14
- name: Install dependencies
# Same extras as the Linux test lane so an OS-marked test can import
@@ -93,7 +93,7 @@ jobs:
# because it could not build here).
uses: ./.github/actions/retry
with:
command: uv sync --locked --python 3.11 --extra all --extra dev --extra anthropic --extra mistral --extra fal --extra modal --extra daytona --extra hindsight --extra parallel-web
command: uv sync --locked --python 3.14 --extra all --extra dev --extra anthropic --extra mistral --extra fal --extra modal --extra daytona --extra hindsight --extra parallel-web
- name: Minimize uv cache
run: uv cache prune --ci
+6 -6
View File
@@ -59,10 +59,10 @@ jobs:
pyproject.toml
uv.lock
- name: Set up Python 3.11
- name: Set up Python 3.14
uses: ./.github/actions/retry
with:
command: uv python install 3.11
command: uv python install 3.14
- name: Install dependencies
# `uv sync --locked` installs the exact pinned set from uv.lock (and
@@ -80,7 +80,7 @@ jobs:
# also honors the exact supply-chain pins these extras carry.
uses: ./.github/actions/retry
with:
command: uv sync --locked --python 3.11 --extra all --extra dev --extra anthropic --extra mistral --extra fal --extra modal --extra daytona --extra hindsight --extra parallel-web
command: uv sync --locked --python 3.14 --extra all --extra dev --extra anthropic --extra mistral --extra fal --extra modal --extra daytona --extra hindsight --extra parallel-web
- name: Minimize uv cache
# Optimized for CI: prunes pre-built wheels that are cheap to
@@ -164,8 +164,8 @@ jobs:
pyproject.toml
uv.lock
- name: Set up Python 3.11
run: uv python install 3.11
- name: Set up Python 3.14
run: uv python install 3.14
- name: Install dependencies
# `uv sync --locked` installs the exact pinned set from uv.lock (and
@@ -179,7 +179,7 @@ jobs:
# in the venv up front.
uses: ./.github/actions/retry
with:
command: uv sync --locked --python 3.11 --extra all --extra dev --extra anthropic --extra mistral --extra fal --extra modal --extra daytona --extra hindsight --extra parallel-web
command: uv sync --locked --python 3.14 --extra all --extra dev --extra anthropic --extra mistral --extra fal --extra modal --extra daytona --extra hindsight --extra parallel-web
- name: Minimize uv cache
# Optimized for CI: prunes pre-built wheels that are cheap to
+3 -3
View File
@@ -42,15 +42,15 @@ jobs:
pyproject.toml
uv.lock
- name: Set up Python 3.11
- name: Set up Python 3.14
uses: ./.github/actions/retry
with:
command: uv python install 3.11
command: uv python install 3.14
- name: Install dependencies
uses: ./.github/actions/retry
with:
command: uv sync --locked --python 3.11 --extra dev --extra messaging
command: uv sync --locked --python 3.14 --extra dev --extra messaging
- name: Run venv-holder live E2E
shell: bash
+2 -2
View File
@@ -50,7 +50,7 @@ Run this in PowerShell:
iex (irm https://hermes-agent.nousresearch.com/install.ps1)
```
The installer handles everything: uv, Python 3.11, Node.js, ripgrep, ffmpeg, **and a portable Git Bash** (MinGit, unpacked to `%LOCALAPPDATA%\hermes\git` — no admin required, completely isolated from any system Git install). Hermes uses this bundled Git Bash to run shell commands.
The installer handles everything: uv, Python 3.14, Node.js, ripgrep, ffmpeg, **and a portable Git Bash** (MinGit, unpacked to `%LOCALAPPDATA%\hermes\git` — no admin required, completely isolated from any system Git install). Hermes uses this bundled Git Bash to run shell commands.
If you already have Git installed, the installer detects it and uses that instead. Otherwise a ~45MB MinGit download is all you need — it won't touch or interfere with any system Git.
@@ -239,7 +239,7 @@ against its own checkout, destroying the running runtime mid-session.
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
uv venv ~/.hermes/venvs/hermes-dev --python 3.11
uv venv ~/.hermes/venvs/hermes-dev --python 3.14
source ~/.hermes/venvs/hermes-dev/bin/activate
uv pip install -e ".[all,dev]"
scripts/run_tests.sh
+1 -1
View File
@@ -2849,7 +2849,7 @@ def _show_terminal_section(config: Dict[str, Any]) -> None:
print(f" Timeout: {terminal.get('timeout', 60)}s")
configured = lambda *names: 'configured' if all(get_env_value(n) for n in names) else '(not set)' # noqa: E731
default_img = 'nikolaik/python-nodejs:python3.11-nodejs20'
default_img = 'nikolaik/python-nodejs:python3.14-nodejs22'
backend_lines = {
'docker': lambda: [f" Docker image: {terminal.get('docker_image', default_img)}"],
'singularity': lambda: [f" Image: {terminal.get('singularity_image', 'docker://' + default_img)}"],
+4 -4
View File
@@ -301,15 +301,15 @@ DEFAULT_CONFIG = {
# go first because n/nvm/asdf write PATH exports there without an interactivity guard. Turn
# off if an rc file misbehaves when sourced non-interactively (exits on TTY check).
"auto_source_bashrc": True,
"docker_image": "nikolaik/python-nodejs:python3.11-nodejs20",
"docker_image": "nikolaik/python-nodejs:python3.14-nodejs22",
"docker_forward_env": [],
# Exact key-value env pairs set inside Docker containers (unlike docker_forward_env, which
# reads host values) — useful under systemd without the user's shell env. Example:
# {"SSH_AUTH_SOCK": "/run/user/1000/ssh-agent.sock"}
"docker_env": {},
"singularity_image": "docker://nikolaik/python-nodejs:python3.11-nodejs20",
"modal_image": "nikolaik/python-nodejs:python3.11-nodejs20",
"daytona_image": "nikolaik/python-nodejs:python3.11-nodejs20",
"singularity_image": "docker://nikolaik/python-nodejs:python3.14-nodejs22",
"modal_image": "nikolaik/python-nodejs:python3.14-nodejs22",
"daytona_image": "nikolaik/python-nodejs:python3.14-nodejs22",
"vercel_runtime": "node24", # vercel_sandbox backend only: node24 | node22 | python3.13
# Container limits (docker, singularity, modal, daytona, vercel_sandbox; not local/ssh).
"container_cpu": 1,
+6 -6
View File
@@ -530,14 +530,14 @@ def _install_safe_python_generation(
return result
# All patches on the current minor line are vulnerable or rejected.
# Fall forward to the next supported minor (e.g. 3.11 → 3.12) so the
# user isn't stuck on every `hermes update` with no path to a fixed
# runtime (issue #76106). The requires-python constraint
# (>=3.11,<3.14) and the downstream import smoke-test gate
# Fall forward to the next supported minor (e.g. 3.14 → 3.15 when the
# ceiling rises) so the user isn't stuck on every `hermes update` with
# no path to a fixed runtime (issue #76106). The requires-python
# constraint (>=3.14,<3.15) and the downstream import smoke-test gate
# compatibility; we only need to stay inside that window.
cur_major, cur_minor = current.python_version[:2]
fb_tried: set[tuple[int, int, int]] = set(tried_versions)
for next_minor in range(cur_minor + 1, 14): # up to 3.13
for next_minor in range(cur_minor + 1, 15): # up to 3.14 (<3.15 ceiling)
next_request = f"{cur_major}.{next_minor}"
print(
f" → No fixed {cur_major}.{cur_minor} build available; "
@@ -1182,5 +1182,5 @@ def repair_vulnerable_runtime(
# ---------------------------------------------------------------------------
def rebuild_venv(uv_bin: str, venv_dir: Path, python_version: str = "3.11") -> bool:
def rebuild_venv(uv_bin: str, venv_dir: Path, python_version: str = "3.14") -> bool:
return True # dont remove me. ask ethernet
+15 -15
View File
@@ -300,35 +300,35 @@
"python": {
"artifacts": {
"darwin-arm64": {
"sha256": "fcba9f3f676c83e07225e38116649f0c6eb94cb4fcc166632cf92769462b6e39",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.11.16+20260814-aarch64-apple-darwin-install_only.tar.gz"
"sha256": "30daa970c7d223530120f1693cd3c6fa4c0c0d31ef158710b0dd77f286a5b23e",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260901/cpython-3.14.7+20260901-aarch64-apple-darwin-install_only.tar.gz"
},
"darwin-x64": {
"sha256": "d9117d31251ba5c9a81ac7ad7c00dab1d5228e261eb0cda94550b4da1cc73bd1",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.11.16+20260814-x86_64-apple-darwin-install_only.tar.gz"
"sha256": "dd8841a2e8ef94bd1a02b52f92843120942140f112145d4e0199abab56f120b1",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260901/cpython-3.14.7+20260901-x86_64-apple-darwin-install_only.tar.gz"
},
"linux-arm64": {
"sha256": "9142c12dd3559eaac58a18d8905b99a293979d5e5a1b4fb5cf4cebbf82ef6f33",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.11.16+20260814-aarch64-unknown-linux-gnu-install_only.tar.gz"
"sha256": "30f1cc489be654477d895b441e196bb080738bf0456da82080ad4ab66a22d80f",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260901/cpython-3.14.7+20260901-aarch64-unknown-linux-gnu-install_only.tar.gz"
},
"linux-arm64-bionic": {
"sha256": "61a8df1dfee53de364757aa751faac033725262a4be04f086c3e3fedbe38991d",
"url": "https://tur.kcubeterm.com/pool/tur/python3.11_3.11.16_aarch64.deb"
"sha256": "3166e56c2b6c03fff41191fbb9d736302978e7c484702814d9f6dc99dd6006bd",
"url": "https://packages.termux.dev/apt/termux-main/pool/main/p/python/python_3.14.6-1_aarch64.deb"
},
"linux-x64": {
"sha256": "33994fad90145ba559ebbe8a18d69fa7e56653502f7ba14ba07199b52cde3775",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.11.16+20260814-x86_64-unknown-linux-gnu-install_only.tar.gz"
"sha256": "0ab3305457051cd3e7c031857e005f1bda17c218a1990567dacaaac6dd1d14f0",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260901/cpython-3.14.7+20260901-x86_64-unknown-linux-gnu-install_only.tar.gz"
},
"win32-arm64": {
"sha256": "450cbf91ff0dbfce7fa1d40ba19103187852076496b6153e528fa6dc43a88a58",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.11.16+20260814-aarch64-pc-windows-msvc-install_only.tar.gz"
"sha256": "5efa2548bf1248ca07ed6f8afb0cb52b83d4869d533ea5be919a989c8ee1b17c",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260901/cpython-3.14.7+20260901-aarch64-pc-windows-msvc-install_only.tar.gz"
},
"win32-x64": {
"sha256": "ffaee1e94c8488f833473e56e1c980ca1a58d91126d21ddf0f6ba052e69cf511",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.11.16+20260814-x86_64-pc-windows-msvc-install_only.tar.gz"
"sha256": "5d9242012dded591d723a3a572dda265173ad58d8a3e6fdbc6dac8f94f36c80a",
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260901/cpython-3.14.7+20260901-x86_64-pc-windows-msvc-install_only.tar.gz"
}
},
"version": "3.11.16+20260814"
"version": "3.14.7+20260901"
},
"ripgrep": {
"artifacts": {
+16 -8
View File
@@ -239,12 +239,20 @@ class Python(_BionicDebArm, BinaryPackage, DebPackage):
probe_version = False
binary_rel = {"win32": "python.exe", "posix": "bin/python3"}
# The staged .deb's main binary: DebPackage.verify checks it.
main_bin_rel = "bin/python3.11"
main_bin_rel = "bin/python3.14"
def main_rel(self, target: str) -> str:
return self.main_bin_rel
deb_package = "python3.11"
# termux-main (official termux repo) python deb. It lags python-build-
# standalone by one patch (3.14.6-1 vs 3.14.7), so the bionic row is a
# manual pin -- never derived from the node version, and pm update leaves
# it alone (no bionic resolver).
deb_package = "python"
_BIONIC_URL = (
"https://packages.termux.dev/apt/termux-main/pool/main/p/python/"
"python_3.14.6-1_aarch64.deb"
)
def stage(self, store: Store, staged: Path, version: str, target: str) -> None:
super().stage(store, staged, version, target)
@@ -261,9 +269,8 @@ class Python(_BionicDebArm, BinaryPackage, DebPackage):
def fetch_url(self, version: str, target: str) -> str:
if target == "linux-arm64-bionic":
pyver = version.partition("+")[0]
return f"https://tur.kcubeterm.com/pool/tur/python3.11_{pyver}_aarch64.deb"
# lock version is "<python>+<release tag>", e.g. "3.11.13+202****0807"
return self._BIONIC_URL
# lock version is "<python>+<release tag>", e.g. "3.14.7+20260901"
pyver, _, tag = version.partition("+")
if not tag:
raise InstallError(self.name, f"version {version!r} needs the +<release> tag")
@@ -274,10 +281,11 @@ class Python(_BionicDebArm, BinaryPackage, DebPackage):
)
def latest_versions(self, target: str, locked=None) -> list[str]:
# Stay on the locked python minor line (3.11); bump only the
# Stay on the locked python minor line (3.14); bump only the
# +<build-tag>. A major/minor bump is a deliberate decision, never
# an auto-update.
if not locked or "+" not in locked:
# an auto-update. The bionic row is a manual termux-main pin -- no
# python-build-standalone build exists for it, so leave it locked.
if target == "linux-arm64-bionic" or not locked or "+" not in locked:
return []
pyver = locked.partition("+")[0]
minor = ".".join(pyver.split(".")[:2])
+1 -1
View File
@@ -43,7 +43,7 @@ _VERSION_PART_RE = re.compile(r"\d+|[A-Za-z]+")
def version_key(version: str) -> tuple:
"""A sortable key for a version string. Handles dot-separated numerics,
+suffixes (python's 3.11.16+20260814, git's 2.53.0+3), and plain build
+suffixes (python's 3.14.7+20260901, git's 2.53.0+3), and plain build
numbers (llamacpp's 10362). Non-numeric segments sort after numerics so
a prerelease never beats its release."""
key = []
+1 -1
View File
@@ -72,7 +72,7 @@ def make_maker(bin_dir, shebang_python, wrapper_text):
def mint_one(bin_dir, shebang_python, wrapper_text, spec):
"""Mint one launcher exe; returns its absolute path. distlib also
writes a python-versioned twin (hermes-3.11.exe) — the payload ships
writes a python-versioned twin (hermes-3.14.exe) — the payload ships
exactly one name per entry, so the twin is removed."""
maker = make_maker(bin_dir, shebang_python, wrapper_text)
filenames = maker.make(f"{spec['name']} = {spec['module']}:{spec['func']}")
@@ -115,7 +115,7 @@ try {
Copy-Item (Join-Path $repoRoot 'hermes_constants.py') (Join-Path $installRoot 'hermes_constants.py')
$store = Join-Path $caseRoot 'store'
$entryName = 'python-3.11.15+x20260807-win32-arm64'
$entryName = 'python-3.14.7+x20260901-win32-arm64'
New-Item -ItemType Directory -Force -Path (Join-Path $store $entryName) | Out-Null
Copy-Item $python (Join-Path $store "$entryName\python.exe")
('{"schema": 1, "packages": {"python": {"entry": "' + $entryName + '"}}}') |
+1 -1
View File
@@ -24,7 +24,7 @@ export function generateIcons(args = [], { root = repoRoot, run = spawnSync, env
delete childEnv.PYTHONPATH
delete childEnv.PYTHONHOME
const result = run('uv', [
'run', '--isolated', '--locked', '--only-group', 'icon-build', '--python', '3.11',
'run', '--isolated', '--locked', '--only-group', 'icon-build', '--python', '3.14',
// PM copies its wheel cache into payloads. Keep build wheels outside it.
'--cache-dir', path.join(root, '.cache', 'icon-build'),
'python', path.join(root, 'scripts', 'generate_icons.py'), ...args
+1 -1
View File
@@ -505,7 +505,7 @@ function Invoke-BootstrapPm {
$uv = Get-Uv
$lock = Get-Content (Join-Path $InstallDir "pm\lock.json") -Raw | ConvertFrom-Json
$pyPin = $lock.packages.python
$pyVersion = if ($pyPin) { ($pyPin.version -split '\+')[0] -replace '^(\d+\.\d+).*', '$1' } else { '3.11' }
$pyVersion = if ($pyPin) { ($pyPin.version -split '\+')[0] -replace '^(\d+\.\d+).*', '$1' } else { '3.14' }
Log "delegating python + venv + tools to pm (hash-verified via uv.lock)"
Push-Location $InstallDir
try {
+1 -1
View File
@@ -289,7 +289,7 @@ bootstrap_pm() {
_py="$(awk '/^ "python": \{/ { in_py = 1 }
in_py && /^ "version":/ { gsub(/.*: "|"$|",$/, ""); print; exit }' \
"$INSTALL_DIR/pm/lock.json" | cut -d+ -f1 | cut -d. -f1,2)"
[ -n "$_py" ] || _py="3.11"
[ -n "$_py" ] || _py="3.14"
log "delegating python + venv + tools to pm (hash-verified via uv.lock)"
(cd "$INSTALL_DIR" && "$UV_CMD" run --no-project --python "$_py" python -m pm.cli install) \
|| fail "pm install failed"
+1 -1
View File
@@ -14,7 +14,7 @@ PYTHON_ENTRY=$(node -e "
" "$PWD")
case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*) PY="$PWD/tools/$PYTHON_ENTRY/python.exe"; SP="$PWD/venv/Lib/site-packages" ;;
*) PY="$PWD/tools/$PYTHON_ENTRY/bin/python3"; SP="$PWD/venv/lib/python3.11/site-packages" ;;
*) PY="$PWD/tools/$PYTHON_ENTRY/bin/python3"; SP="$PWD/venv/lib/python3.14/site-packages" ;;
esac
[ -f "$PY" ] || { echo "FAIL: no store interpreter at $PY"; exit 1; }
+2 -2
View File
@@ -12,9 +12,9 @@
PLATFORM_TAG="android_24_arm64_v8a"
# ABI tag of the bundled interpreter, derived from the pm python version
# (cp311 for the 3.11.x line the lock pins). Kept beside the platform tag
# (cp314 for the 3.14.x line the lock pins). Kept beside the platform tag
# because index.json's pythonAbi field must agree with it.
PYTHON_ABI="cp311"
PYTHON_ABI="cp314"
# Toolchain pins for the psutil patched-local build path (--no-build-
# isolation). Pure-python installs only: maturin is deliberately absent --
+2 -2
View File
@@ -70,7 +70,7 @@ COMMIT="$(git -C "$REPO_ABS" rev-parse --verify "refs/tags/$TAG^{commit}")" \
for d in python node uv npm ffmpeg ripgrep runtime-libs app wheelhouse; do
[ -d "$PAYLOAD_ABS/$d" ] || fail "payload missing $d/ -- run termux_build.sh + build_cpython.sh + build_node.sh first"
done
PYBIN_REL="data/data/com.termux/files/usr/bin/python3.11"
PYBIN_REL="data/data/com.termux/files/usr/bin/python3.14"
[ -f "$PAYLOAD_ABS/python/$PYBIN_REL" ] || fail "payload python tree lacks $PYBIN_REL"
NODEBIN_REL="data/data/com.termux/files/usr/bin/node"
[ -f "$PAYLOAD_ABS/node/$NODEBIN_REL" ] || fail "payload node tree lacks $NODEBIN_REL"
@@ -130,7 +130,7 @@ docker run --rm --platform linux/arm64 \
# The staged tree is mounted at its REAL $PREFIX path so the venv
# recorded absolute paths are correct on-device from birth.
mkdir -p "$PREFIX" 2>/dev/null || true
PY="$PREFIX/lib/hermes-agent/tools/python$PREFIX/bin/python3.11"
PY="$PREFIX/lib/hermes-agent/tools/python$PREFIX/bin/python3.14"
UV="$PREFIX/lib/hermes-agent/tools/uv$PREFIX/bin/uv"
# Desktop payload canon: the venv holds the DEPENDENCY tree only;
# the app runs from its own directory via PYTHONPATH (the wheel
+7 -4
View File
@@ -218,15 +218,18 @@ def build_wheels(build_set: list[str], specs: dict[str, str], wheelhouse: Path)
TARGET_ENV = {
"implementation_name": "cpython",
"implementation_version": "3.11.15",
"implementation_version": "3.14.6",
"os_name": "posix",
"platform_machine": "aarch64",
"platform_release": "",
"platform_system": "Linux",
"platform_version": "",
"python_full_version": "3.11.15",
"python_version": "3.11",
"sys_platform": "linux",
"python_full_version": "3.14.6",
"python_version": "3.14",
# 3.13+ Android CPython reports sys.platform "android" (docs: changed in
# 3.13), so markers keying on `sys_platform == 'linux'` no longer admit
# the termux target; `platform_system` stays "Linux" (Android kernel).
"sys_platform": "android",
}
# Mirrors the documented misses in termux_build.sh's probe (single
+1 -1
View File
@@ -5,7 +5,7 @@ The sealed termux deb is self-contained by contract: the device's termux
tree may have NONE of the payload interpreters' runtime libs installed
(first real-device install: `import ctypes` dlopened libffi.so and died).
The pin table (runtime_libs.json) is derived from the suppliers' own
dependency metadata -- the TUR python3.11 .deb's Depends line, uv's zstd,
dependency metadata -- the termux-main python .deb's Depends line, uv's zstd,
nodejs's libc++/c-ares/libicu -- not from whichever lib happens to error
first.
+12 -10
View File
@@ -39,7 +39,7 @@ if [ "${1:-}" = "--in-container" ]; then
# interpreter the phone will run.
PAYLOAD_ROOT="${5:-}"
export PREFIX=/data/data/com.termux/files/usr
STAGED_PY="$PAYLOAD_ROOT/python$PREFIX/bin/python3.11"
STAGED_PY="$PAYLOAD_ROOT/python$PREFIX/bin/python3.14"
STAGED_UV="$PAYLOAD_ROOT/uv$PREFIX/bin/uv"
# The staged binaries' RUNPATHs point at the phone's $PREFIX layout
# (linkerconfig on-device). Inside the container the tree lives at
@@ -88,7 +88,7 @@ if [ "${1:-}" = "--in-container" ]; then
export UV_CONCURRENT_BUILDS=1
# Native extension links need the STAGED payload's libpython: the
# container's own $PREFIX/lib (bootstrap only) is on the default
# -L path, but libpython3.11.so lives in the staged tree. setuptools
# -L path, but libpython3.14.so lives in the staged tree. setuptools
# honors LDFLAGS, so every sdist build's link step finds it.
STAGED_PYLIB="$PAYLOAD_ROOT/python$PREFIX/lib"
export LDFLAGS="-L$STAGED_PYLIB ${LDFLAGS:-}"
@@ -259,20 +259,22 @@ import json, re, sys, urllib.request
from concurrent.futures import ThreadPoolExecutor
# The TARGET environment the wheelhouse must satisfy: Termux's bionic
# python. TUR 3.11 reports sys.platform "linux" (the android value only
# arrived in 3.13), so markers keying on linux admit it -- and windows/
# darwin markers exclude it, which is the whole point.
# python. termux-main 3.14 reports sys.platform "android" (the linux value
# applied to 3.11/3.12; 3.13 changed it), so markers keying on
# `sys_platform == 'linux'` no longer admit the termux target -- and
# `platform_system` stays "Linux" (Android kernel), admitting
# platform_system-gated deps. windows/darwin markers exclude it as before.
TARGET_ENV = {
"implementation_name": "cpython",
"implementation_version": "3.11.15",
"implementation_version": "3.14.6",
"os_name": "posix",
"platform_machine": "aarch64",
"platform_release": "",
"platform_system": "Linux",
"platform_version": "",
"python_full_version": "3.11.15",
"python_version": "3.11",
"sys_platform": "linux",
"python_full_version": "3.14.6",
"python_version": "3.14",
"sys_platform": "android",
}
def locked_version(spec: str) -> str | None:
@@ -358,7 +360,7 @@ fi
# interpreter by construction. The payload must be staged before this.
log "Building the wheelhouse inside the pinned container (payload ABI)"
PAYLOAD_ABS="$OUT_ABS"
[ -f "$PAYLOAD_ABS/python/data/data/com.termux/files/usr/bin/python3.11" ] \
[ -f "$PAYLOAD_ABS/python/data/data/com.termux/files/usr/bin/python3.14" ] \
|| fail "staged payload python missing -- run build_cpython.sh first (the wheelhouse builds with the payload interpreter)"
# The container mounts OUT_ABS at /out; translate the host-side work
# paths before crossing the boundary (host absolutes do not exist inside).
+9 -8
View File
@@ -681,15 +681,15 @@ class TestMinorLineFallForward:
def test_returns_none_with_bounded_attempts_when_all_minors_exhausted(
self, tmp_path, monkeypatch
):
"""When every build on every supported minor line (3.11-3.13) is
"""When every build on every supported minor line (3.11-3.14) is
vulnerable, the provisioner must give up with None -- and the total
install workload must stay bounded by _MAX_PATCH_RETRIES per line."""
import hermes_cli.runtime_repair as runtime_repair
install_calls = []
resolutions = {"3.11": (3, 11, 14), "3.12": (3, 12, 30), "3.13": (3, 13, 30)}
resolutions = {"3.11": (3, 11, 14), "3.12": (3, 12, 30), "3.13": (3, 13, 30), "3.14": (3, 14, 30)}
patch_lists = {}
for minor in (11, 12, 13):
for minor in (11, 12, 13, 14):
versions = [(3, minor, v) for v in range(30, 10, -1)] # 20 patches
patch_lists[f"3.{minor}"] = versions
for version in versions:
@@ -713,13 +713,14 @@ class TestMinorLineFallForward:
cap = runtime_repair._MAX_PATCH_RETRIES
# Per line: one bare request + at most _MAX_PATCH_RETRIES explicit
# patches; three lines total (3.11, 3.12, 3.13) and nothing beyond
# 3.13 (requires-python is <3.14).
# patches; four lines total (3.11, 3.12, 3.13, 3.14) and nothing beyond
# 3.14 (requires-python is <3.15).
assert install_calls.count("3.11") == 1
assert install_calls.count("3.12") == 1
assert install_calls.count("3.13") == 1
assert not any(call.startswith("3.14") for call in install_calls)
for minor in (11, 12, 13):
assert install_calls.count("3.14") == 1
assert not any(call.startswith("3.15") for call in install_calls)
for minor in (11, 12, 13, 14):
explicit = [
call for call in install_calls
if call.startswith(f"3.{minor}.")
@@ -727,7 +728,7 @@ class TestMinorLineFallForward:
assert len(explicit) <= cap, (
f"3.{minor} explicit retries must be capped at {cap}: {explicit}"
)
assert len(install_calls) <= 3 * (1 + cap)
assert len(install_calls) <= 4 * (1 + cap)
class TestListAvailablePatches:
+2 -2
View File
@@ -53,7 +53,7 @@ def _fake_repo(tmp_path: Path, monkeypatch) -> Path:
"""A fake repo root with a synced venv, substituted for pm.paths.repo_root."""
repo = tmp_path / "repo"
win = current_target().startswith("win32")
site = repo / "venv" / ("Lib" if win else "lib/python3.11") / "site-packages"
site = repo / "venv" / ("Lib" if win else "lib/python3.14") / "site-packages"
site.mkdir(parents=True)
monkeypatch.setattr(paths, "repo_root", lambda: repo)
return repo
@@ -63,7 +63,7 @@ def _develop_env(tmp_path, monkeypatch, *, with_python=True):
_fake_store(tmp_path, monkeypatch, with_python=with_python)
repo = _fake_repo(tmp_path, monkeypatch)
win = current_target().startswith("win32")
site = repo / "venv" / ("Lib" if win else "lib/python3.11") / "site-packages"
site = repo / "venv" / ("Lib" if win else "lib/python3.14") / "site-packages"
return pm_cli._develop_env(["faketool"]), repo, site
+6 -6
View File
@@ -490,12 +490,12 @@ def test_python_package_url_carries_release_tag():
from pm.registry import get_package
python = get_package("python")
url = python.fetch_url("3.11.16+20260814", "win32-arm64")
assert "download/20260814/" in url
assert "cpython-3.11.16+20260814-aarch64-pc-windows-msvc-install_only" in url
url = python.fetch_url("3.14.7+20260901", "win32-arm64")
assert "download/20260901/" in url
assert "cpython-3.14.7+20260901-aarch64-pc-windows-msvc-install_only" in url
try:
python.fetch_url("3.11.16", "win32-arm64")
python.fetch_url("3.14.7", "win32-arm64")
raise AssertionError("bare version must be rejected")
except PmInstallError:
pass
@@ -742,7 +742,7 @@ def test_python_stage_drops_unloadable_x64_vc_runtime_on_arm64(monkeypatch, tmp_
(staged / "vcruntime140.dll").write_bytes(b"arm64")
monkeypatch.setattr(packages, "_macos_sign_managed_python", lambda p: False)
get_package("python").stage(None, staged, "3.11.16", "win32-arm64")
get_package("python").stage(None, staged, "3.14.7", "win32-arm64")
assert not (staged / "vcruntime140_1.dll").exists()
assert (staged / "vcruntime140.dll").is_file()
@@ -757,7 +757,7 @@ def test_python_stage_keeps_vc_runtimes_on_other_targets(monkeypatch, tmp_path):
(staged / "vcruntime140_1.dll").write_bytes(b"x64")
monkeypatch.setattr(packages, "_macos_sign_managed_python", lambda p: False)
get_package("python").stage(None, staged, "3.11.16", "win32-x64")
get_package("python").stage(None, staged, "3.14.7", "win32-x64")
assert (staged / "vcruntime140_1.dll").is_file()
+2 -2
View File
@@ -37,7 +37,7 @@ def _pkg(pkg_name="node", version_style="semver", **latest):
def test_version_key_sorts_numeric_and_suffixes():
assert version_key("2.53.0+5") > version_key("2.53.0+3")
assert version_key("3.11.16+20260814") > version_key("3.11.16+20260801")
assert version_key("3.14.7+20260901") > version_key("3.14.7+20260900")
assert version_key("26.8.1") > version_key("26.7.0")
assert version_key("10362") > version_key("10361")
# prerelease-ish segments sort after numerics
@@ -46,7 +46,7 @@ def test_version_key_sorts_numeric_and_suffixes():
def test_minor_of():
assert minor_of("26.7.0") == (26, 7)
assert minor_of("3.11.16+20260814") == (3, 11)
assert minor_of("3.14.7+20260901") == (3, 14)
assert minor_of("10362") is None # single component
+3 -3
View File
@@ -34,7 +34,7 @@ We value contributions in this order:
| Requirement | Notes |
| -------------------- | --------------------------------------------------------------------------------------------- |
| **Git** | With the `git-lfs` extension installed |
| **Python 3.11–3.13** | uv will install it if missing |
| **Python 3.14** | uv will install it if missing |
| **uv** | Fast Python package manager ([install](https://docs.astral.sh/uv/)) |
| **Node.js 26+** | Optional — needed for browser tools and WhatsApp bridge (matches root `package.json` engines) |
@@ -92,8 +92,8 @@ tree means no relative path from the workspace resolves to it.
git clone https://github.com/NousResearch/hermes-agent.git
cd hermes-agent
# Create venv with Python 3.11, OUTSIDE the source tree
uv venv ~/.hermes/venvs/hermes-dev --python 3.11
# Create venv with Python 3.14, OUTSIDE the source tree
uv venv ~/.hermes/venvs/hermes-dev --python 3.14
export VIRTUAL_ENV="$HOME/.hermes/venvs/hermes-dev"
export PATH="$VIRTUAL_ENV/bin:$PATH"
+1 -1
View File
@@ -93,7 +93,7 @@ You don't need to rebuild your setup from scratch. Restore a full backup with `h
**Installer:** On non-Windows platforms, the only prerequisite is **Git**. On Linux, also make sure `curl` and `xz-utils` are available (the installer downloads Node.js as a `.tar.xz` archive). The desktop app additionally requires `g++` (or `build-essential` on Debian/Ubuntu) to compile native modules. The installer automatically handles everything else:
- **uv** (fast Python package manager)
- **Python 3.11** (via uv, no sudo needed)
- **Python 3.14** (via uv, no sudo needed)
- **Node.js v26** (for browser automation and WhatsApp bridge; existing system Node 22.22+, 24.11+, or 26+ is used as-is)
- **ripgrep** (fast file search)
- **ffmpeg** (audio format conversion for TTS)
+1 -1
View File
@@ -146,7 +146,7 @@ The installer adds `~/.local/bin` to your PATH. If you use a non-standard shell
#### Python version too old
**Cause:** Hermes requires Python 3.11 or newer.
**Cause:** Hermes requires Python 3.14 or newer.
**Solution:**
```bash
@@ -126,8 +126,7 @@ wake_word:
confirmation_frames: 3 # openWakeWord only — consecutive over-threshold frames required to fire
start_new_session: true # start a fresh session on wake vs. continue the current one
openwakeword:
model: hey_hermes # bundled default; OR a built-in name OR a path to a custom .onnx/.tflite
inference_framework: "" # "" (auto) | "onnx" | "tflite"
model: hey_hermes # bundled default; OR a built-in name OR a path to a custom .tflite
porcupine:
keyword: jarvis # built-in keyword OR path to a custom .ppn
```
@@ -165,13 +164,12 @@ The `sherpa` and `porcupine` engines decode the whole phrase internally, so they
don't have the single-frame-spike problem and ignore `confirmation_frames`
(but they still honor `sensitivity`).
`inference_framework` picks the openWakeWord backend. Leave it empty (the
default) to let Hermes choose per platform: **tflite on Apple Silicon**, onnx
everywhere else. openWakeWord's onnx backend returns near-zero scores on macOS
ARM64 ([openWakeWord#336](https://github.com/dscripka/openWakeWord/issues/336)),
so a listener pinned to `onnx` there will arm, show as listening, and never
fire. The tflite backend needs `ai-edge-litert` on macOS, which Hermes installs
on demand alongside the other wake-word deps.
The `openwakeword` engine is [pyopen-wakeword](https://github.com/rhasspy/pyopen-wakeword)
(rhasspy's maintained fork of openWakeWord): it runs TFLite via a library
bundled in its wheel and ships the same shared feature models openWakeWord
downloaded at runtime (byte-identical, verified by hash), so the shipped
`hey_hermes.tflite` scores exactly as before — with no runtime download and no
backend to pick. There is no `inference_framework` setting anymore.
### Surfaces (CLI, TUI, GUI)
+1 -1
View File
@@ -68,7 +68,7 @@ Each dep has a `shutil.which(...)`-style check; if a binary is missing and the r
Top-to-bottom, in order:
1. **Bootstraps `uv`** — Astral's fast Python manager. Installed to `%USERPROFILE%\.local\bin`.
2. **Installs Python 3.11** via `uv`. No existing Python needed.
2. **Installs Python 3.14** via `uv`. No existing Python needed.
3. **Installs Node.js 26** (winget if available, else a portable Node tarball unpacked under `%LOCALAPPDATA%\hermes\node`). Used for the browser tool and the WhatsApp bridge.
4. **Installs portable Git** — if `git` is already on PATH the installer uses it; otherwise it downloads a trimmed, self-contained **PortableGit** (~45 MB, from the official `git-for-windows` release) to `%LOCALAPPDATA%\hermes\git`. No admin, no Windows installer registry, no interference with anything else on the box.
5. **Clones the repo** to `%LOCALAPPDATA%\hermes\hermes-agent` and creates a virtualenv inside it.