From d354ca62e6852b36abe208123a7cdf7cd4ef89fa Mon Sep 17 00:00:00 2001
From: Talos Kernel
Date: Sun, 25 Oct 2026 10:19:25 +0100
Subject: [PATCH] Secure installation with hash-locked pip and isolated package
operations; prepare beta.8
---
CHANGELOG.md | 12 ++++++++++++
CLAUDE.md | 7 +++++--
README.md | 8 +++++---
macos/app-version.json | 4 ++--
requirements-verifier.lock | 3 +++
requirements.lock | 4 ++++
requirements.txt | 1 +
scripts/beta-clean-install.py | 14 +++++++++++++-
scripts/beta-upgrade-rehearsal.py | 2 +-
scripts/sync-verifier-lock.py | 2 +-
site/console.html | 10 +++++-----
site/docs/index.html | 4 ++--
site/index.html | 4 ++--
site/install.sh | 14 +++++++++-----
site/llms.txt | 2 +-
site/redteam/index.html | 2 +-
site/registry/index.html | 2 +-
site/setup/index.html | 2 +-
site/status.json | 8 ++++----
site/vergleich/index.html | 2 +-
talos/__init__.py | 2 +-
talos/updater.py | 2 +-
tests/test_requirements_lock.py | 13 ++++++++++++-
tests/test_updater.py | 2 ++
24 files changed, 90 insertions(+), 36 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 06b0390..13a551f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,18 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
The 0.20.0 beta line follows the frozen operator contract; earlier alpha releases
remain historical snapshots. Beta does not mean error-free operation.
+## [0.20.0-beta.8] — 2026-10-25
+
+### Security
+
+- Hash-lock pip 26.2.2 in the runtime and temporary signature-verifier environments.
+ Fresh virtual environments previously retained the pip version bundled with the
+ operator's Python, which could remain vulnerable even when application locks were clean.
+ Installation and update now replace that seed through the signed/pinned locks;
+ no unpinned self-upgrade or system-Python modification is introduced.
+- Installer and updater pip subprocesses ignore user configuration and Python import overrides, preventing stale duplicate metadata or redirected installs. Older updaters with custom pip settings can upgrade once using `PIP_CONFIG_FILE=/dev/null talos update`.
+- Retains all Beta.7 task-recovery and monitoring-guidance fixes.
+
## [0.20.0-beta.7] — 2026-10-25
### Fixed
diff --git a/CLAUDE.md b/CLAUDE.md
index fc2c340..faa7f76 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -14,7 +14,7 @@ preferences live in `USER.md`. All three are operator-owned prompt state and rel
|---|---|
| Gate path | `policy.py`, **996 lines** — has to stay readable in one sitting |
| Tools | **37**, every one gated |
-| Suites | **3437** collected tests · **277** adversarial · 44 end-to-end |
+| Suites | **3438** collected tests · **277** adversarial · 44 end-to-end |
| Home | · docs at `/docs/` |
| Repository | `talos-kernel/talos` is the public source tree |
@@ -308,7 +308,7 @@ repairs; corrections never replay an effect or grant permission. Keep those prop
```bash
python3 -m venv .venv && . .venv/bin/activate && pip install --require-hashes -r requirements.lock -r requirements-dev.lock
-python -m pytest tests/ -q # 3437 collected tests; platform-specific skips are explicit
+python -m pytest tests/ -q # 3438 collected tests; platform-specific skips are explicit
python redteam.py # 277 adversarial cases — mandatory for any kernel change
python e2e.py # 44 cases against a real model (costs tokens and time)
python -m talos --once # single cycle, for diagnosis
@@ -352,6 +352,9 @@ osv-scanner scan source --no-resolve \
-L requirements.txt:deploy/computer-requirements.lock
```
+The runtime and verifier locks include pip itself. Audit actual installed distributions as well
+as application locks: the interpreter can otherwise contribute an outdated bootstrap tool.
+
Automatic discovery scans the range files and misses these nonstandard lock names.
Record the scan date and result; a previous clean scan does not cover new advisories.
diff --git a/README.md b/README.md
index 479a8fd..99d74b1 100644
--- a/README.md
+++ b/README.md
@@ -21,10 +21,10 @@
-
-
+
@@ -74,6 +74,8 @@ security update. See the [changelog](CHANGELOG.md#0200-beta6--2026-10-24).
**Source versus release:** `main` can contain newer changes. Signed installers and
package managers follow their pinned release; older archives remain immutable.
+Beta.8 also pins pip 26.2.2 with hashes, including the temporary verifier, and isolates pip from per-user configuration. When upgrading from Beta.7 or older with custom pip settings, use `PIP_CONFIG_FILE=/dev/null talos update` once; those older updaters still inherit pip configuration.
+
## Is Talos right for you?
- **Run local models:** connect Ollama on hardware you control, or use a configured cloud provider.
@@ -379,7 +381,7 @@ come from `stdin` instead of `getUpdates`.
```
$ talos chat
- ◉ Talos / 0.20.0-beta.7
+ ◉ Talos / 0.20.0-beta.8
Your terminal. Your rules.
──────────────────────────────────────────────────────────────────────────
model claude-cli/claude-opus-4-1
diff --git a/macos/app-version.json b/macos/app-version.json
index 2a3a399..d89fdbf 100644
--- a/macos/app-version.json
+++ b/macos/app-version.json
@@ -1,4 +1,4 @@
{
- "version": "0.3.6",
- "build": "12"
+ "version": "0.3.7",
+ "build": "13"
}
diff --git a/requirements-verifier.lock b/requirements-verifier.lock
index 2b1ac74..4bfbf80 100644
--- a/requirements-verifier.lock
+++ b/requirements-verifier.lock
@@ -147,6 +147,9 @@ cryptography==50.0.1 \
--hash=sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80 \
--hash=sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558 \
--hash=sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef
+pip==26.2.2 \
+ --hash=sha256:3a8e5c800492787f33a4114ef54444546c340df428974ebba17e0fd47e11af2f \
+ --hash=sha256:fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70
pycparser==3.0 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' \
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
diff --git a/requirements.lock b/requirements.lock
index b151492..31696bc 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -827,6 +827,10 @@ packaging==26.3 \
# via
# huggingface-hub
# onnxruntime
+pip==26.2.2 \
+ --hash=sha256:3a8e5c800492787f33a4114ef54444546c340df428974ebba17e0fd47e11af2f \
+ --hash=sha256:fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70
+ # via -r requirements.txt
primp==2.0.0 \
--hash=sha256:0776fd400adcc940e47bb70896fb474f6607ffa545f5f7107ccda8d055fa02c9 \
--hash=sha256:0ea78a7a2fc5d48a825fa73182911b956bcb58940297cbd60958963ec30375d4 \
diff --git a/requirements.txt b/requirements.txt
index e75c6f5..fd0c8f6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -16,3 +16,4 @@ anyio>=4.14.2 # transitiv ueber httpx. Untergrenze 4.14.2: alles
certifi>=2026.10.19 # transitiv ueber requests/httpx. Untergrenze schliesst
# PYSEC-2026-4320 / GHSA-6q48-6c7w-9gw2 (CVSS 9.1).
filelock>=4.2.1 # Transitive transcription-cache dependency: October locking security fixes.
+pip>=26.2.0 # Hash-lock the installer itself; do not inherit a vulnerable Python seed.
diff --git a/scripts/beta-clean-install.py b/scripts/beta-clean-install.py
index 20df78f..31e79c3 100644
--- a/scripts/beta-clean-install.py
+++ b/scripts/beta-clean-install.py
@@ -3,6 +3,8 @@
from __future__ import annotations
import argparse
+import json
+import re
import os
import stat
import subprocess
@@ -50,6 +52,16 @@ def main() -> int:
if version.returncode or version.stdout.strip() != args.current:
print("Installed version read-back failed", file=sys.stderr)
return 1
+ expected_pip = re.search(r"^pip==([^\s\\]+)", (ROOT / "requirements.lock").read_text(), re.M).group(1)
+ pip_probe = subprocess.run(
+ [str(prefix / ".venv/bin/python"), "-I", "-c",
+ "import json,pip,importlib.metadata as m;print(json.dumps({"
+ "'module':pip.__version__,'distributions':[d.version for d in m.distributions() "
+ "if d.metadata['Name'].lower()=='pip']}))"],
+ capture_output=True, text=True, check=True)
+ if json.loads(pip_probe.stdout) != {"module": expected_pip, "distributions": [expected_pip]}:
+ print("Installed pip differs from its lock or has stale duplicate metadata", file=sys.stderr)
+ return 1
config = prefix / "talos.env"
if not config.is_file() or stat.S_IMODE(config.stat().st_mode) != 0o600:
print("Installed configuration mode read-back failed", file=sys.stderr)
@@ -57,7 +69,7 @@ def main() -> int:
if not (bin_dir / "talos").is_symlink() or not (prefix / "data").is_dir():
print("Installed command or data directory missing", file=sys.stderr)
return 1
- print(f"Clean install: {args.current}, suites passed, config 0600, command and data read back")
+ print(f"Clean install: {args.current}, suites passed, config 0600, command and data read back; pip {expected_pip} unique")
return 0
diff --git a/scripts/beta-upgrade-rehearsal.py b/scripts/beta-upgrade-rehearsal.py
index a3a223b..95dc4fa 100644
--- a/scripts/beta-upgrade-rehearsal.py
+++ b/scripts/beta-upgrade-rehearsal.py
@@ -77,7 +77,7 @@ def _extract_old(payload: bytes, destination: Path) -> None:
def _python(code: str, tree: Path, *args: str) -> subprocess.CompletedProcess[str]:
environment = {key: os.environ[key] for key in
("PATH", "HOME", "TMPDIR", "LANG", "LC_ALL", "SSL_CERT_FILE",
- "REQUESTS_CA_BUNDLE")
+ "REQUESTS_CA_BUNDLE", "PIP_CONFIG_FILE")
if key in os.environ}
environment.update({"PIP_NO_INPUT": "1", "PIP_DISABLE_PIP_VERSION_CHECK": "1",
"PYTHONNOUSERSITE": "1"})
diff --git a/scripts/sync-verifier-lock.py b/scripts/sync-verifier-lock.py
index 1f0d20d..0de1a15 100644
--- a/scripts/sync-verifier-lock.py
+++ b/scripts/sync-verifier-lock.py
@@ -10,7 +10,7 @@
def verifier_lock(runtime: str) -> str:
- wanted = {"cryptography", "cffi", "pycparser"}
+ wanted = {"cryptography", "cffi", "pycparser", "pip"}
blocks = re.split(r"(?=^[a-zA-Z0-9][a-zA-Z0-9_.-]*==)", runtime, flags=re.M)
selected = []
found = set()
diff --git a/site/console.html b/site/console.html
index 1a7258b..148adcb 100644
--- a/site/console.html
+++ b/site/console.html
@@ -365,7 +365,7 @@
-
talos 0.20.0-beta.7 · operator console · MIT
+
talos 0.20.0-beta.8 · operator console · MIT
recorded · unfurled · no live wire
Witness one deed from door to ledger.
This hall unfurls one true session, recorded on the working machine
@@ -378,7 +378,7 @@
Witness one deed from door to ledger.
This candidate, measured
-
3437automated tests
+
3438automated tests
277/277adversarial
996lines · gate
37gated tools
@@ -553,12 +553,12 @@
Numbers this candidate must ship.
embedded below. Either way the digits come from a count, not from memory.
-
0.20.0-beta.7
beta release
public installer v0.20.0-beta.7
-
3437
automated tests, collected by pytest
+
0.20.0-beta.8
beta release
public installer v0.20.0-beta.8
+
3438
automated tests, collected by pytest
277
adversarial cases, all behaving
996
lines in policy.py, readable in one sitting
37
tools, every one through the gate
-
v0.20.0-beta.7
release tag
+
v0.20.0-beta.8
release tag
diff --git a/site/docs/index.html b/site/docs/index.html
index b23f076..da7df51 100644
--- a/site/docs/index.html
+++ b/site/docs/index.html
@@ -244,7 +244,7 @@
Talos AI agent: the field manual
That one decision shapes the rest of this manual. It is why the
command list is short, and why a few obvious conveniences are missing on purpose.
-
3437automated tests
+
3438automated tests
277/277adversarial
996gate lines
37gated tools
@@ -407,7 +407,7 @@
Section 04Run it
Section 05The terminal session
$ talos chat
-Talos 0.20.0-beta.7 · anthropic-api/claude-opus-4-8 · autonomy 3
+Talos 0.20.0-beta.8 · anthropic-api/claude-opus-4-8 · autonomy 3
speaking as cli:1000 · approvals possible · you are at a terminal
/help for commands, `exit` to leave
diff --git a/site/index.html b/site/index.html
index f234fd4..711b92c 100644
--- a/site/index.html
+++ b/site/index.html
@@ -639,7 +639,7 @@ Measured, with the path stated
None of this asks for belief. The installer runs the unit and
adversarial suites in front of you; the real-model cases are a separate opt-in E2E run.
Beta.6 source and signed release. Counts describe the source test suite, not speed or real-world success rates; signed release contents follow their tag.
-
0
automated tests collected from the source tree
+
0
automated tests collected from the source tree
0
adversarial cases trying to pass the kernel
0
opt-in end-to-end cases against a real model
0
lines in the file that decides every verdict
@@ -764,7 +764,7 @@
Talos AI agent: your questions
raise the keep
The command remains yours
-
Beta release 0.20.0-beta.7. Tested on Python 3.11 · 3.13, Linux and macOS, including Raspberry Pi.
+
Beta release 0.20.0-beta.8. Tested on Python 3.11 · 3.13, Linux and macOS, including Raspberry Pi.
Bring your own model: a supported CLI, configured API provider or local Ollama.
The installer runs the unit and adversarial suites in front of you, then starts nothing.
diff --git a/site/install.sh b/site/install.sh
index 37e9bf0..833e05b 100755
--- a/site/install.sh
+++ b/site/install.sh
@@ -15,7 +15,7 @@
set -euo pipefail
-VERSION="0.20.0-beta.7"
+VERSION="0.20.0-beta.8"
BASE="${TALOS_BASE:-https://talos-agent.ch}"
TARBALL="${BASE}/dist/talos-${VERSION}.tar.gz"
PREFIX="${TALOS_PREFIX:-$HOME/talos}"
@@ -326,12 +326,15 @@ cryptography==50.0.1 \
--hash=sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80 \
--hash=sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558 \
--hash=sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef
+pip==26.2.2 \
+ --hash=sha256:3a8e5c800492787f33a4114ef54444546c340df428974ebba17e0fd47e11af2f \
+ --hash=sha256:fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70
pycparser==3.0 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' \
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
VERIFIER_LOCK
# END PINNED VERIFIER
-spin "verifier deps" "$TMP/verify/bin/python" -m pip install --quiet --require-hashes --only-binary=:all: --no-deps -r "$TMP/verifier.lock" \
+spin "verifier deps" "$TMP/verify/bin/python" -I -m pip --isolated install --quiet --require-hashes --only-binary=:all: --no-deps -r "$TMP/verifier.lock" \
|| die "could not install the signature verifier — an install is not the place to skip this."
"$TMP/verify/bin/python" - "$TMP/talos.tar.gz" "$TMP/sig" <<'VERIFY' || die "the release signature does not match this archive. Nothing was unpacked, nothing was changed."
import base64, pathlib, sys
@@ -362,17 +365,18 @@ VPY="$PREFIX/.venv/bin/python"
# Every version pinned, every file hashed, resolved for every platform when the
# release was cut — installed under --require-hashes, so the signature checked
# above also covers what pip fetches now. pip itself is not upgraded first: that
-# would be one unpinned download ahead of all the pinned ones.
+# would be one unpinned download ahead of all the pinned ones. The lock includes pip.
+# Isolated mode ignores per-user pip settings (such as ignore-installed/target).
[ -f "$PREFIX/requirements.lock" ] \
|| die "this archive ships no requirements.lock — refusing to install unpinned dependencies."
-spin "dependencies" "$VPY" -m pip install --quiet --require-hashes -r "$PREFIX/requirements.lock" \
+spin "dependencies" "$VPY" -I -m pip --isolated install --quiet --require-hashes -r "$PREFIX/requirements.lock" \
|| die "dependencies failed — a hash mismatch here means a package did not arrive as released."
# The proofs below need a test runner. It is a separate lock because the agent
# itself never imports it — installing it here is the price of not asking you
# to take the claims on faith.
[ -f "$PREFIX/requirements-dev.lock" ] \
|| die "this archive ships no requirements-dev.lock — the proofs below would run unpinned."
-spin "test runner" "$VPY" -m pip install --quiet --require-hashes -r "$PREFIX/requirements-dev.lock" \
+spin "test runner" "$VPY" -I -m pip --isolated install --quiet --require-hashes -r "$PREFIX/requirements-dev.lock" \
|| die "could not install the test runner — the proofs below would be skipped."
ok "$PREFIX/.venv — no system packages touched"
diff --git a/site/llms.txt b/site/llms.txt
index 9a84afb..d6888a5 100644
--- a/site/llms.txt
+++ b/site/llms.txt
@@ -6,7 +6,7 @@
- Permission kernel: 996 lines of Python, the only code that decides
- 37 tools, all gated; unattended runs cannot escalate
-- 3437 tests collected in the source tree; 277/277 red-team cases passed in source verification. Release contents follow their own signed tag.
+- 3438 tests collected in the source tree; 277/277 red-team cases passed in source verification. Release contents follow their own signed tag.
- MCP servers run only inside a UID-separated worker, from an operator-owned registry — no marketplace, no third-party code in the agent process
- Channels: Telegram, mail (IMAP pull), CLI, WhatsApp via an operator-controlled broker — every channel fetches; no inbound port, no webhook
- Updates: signed tarballs, verified before anything is unpacked; dependencies pinned and hashed, installed with --require-hashes
diff --git a/site/redteam/index.html b/site/redteam/index.html
index f013533..98464d2 100644
--- a/site/redteam/index.html
+++ b/site/redteam/index.html
@@ -224,7 +224,7 @@
277 ways to break an agent.
25attack surfaces
996kernel lines
37gated tools
-
3437automated tests
+
3438automated tests
0passes sold as safety
diff --git a/site/registry/index.html b/site/registry/index.html
index f42394c..c0c56ca 100644
--- a/site/registry/index.html
+++ b/site/registry/index.html
@@ -239,7 +239,7 @@
The MCP registry
guide with the full protocol is
docs/claude-worker.md.
A curated starting point ships as
examples/mcp-servers.json.
-
3437automated tests
+
3438automated tests
277/277adversarial
996gate lines
37gated tools
diff --git a/site/setup/index.html b/site/setup/index.html
index 2cf8b65..8dbc22a 100644
--- a/site/setup/index.html
+++ b/site/setup/index.html
@@ -163,7 +163,7 @@
This address moved.
Return to the keep
-
3437automated tests
+
3438automated tests
277/277adversarial
996gate lines
37gated tools
diff --git a/site/status.json b/site/status.json
index 280cc3b..c95c957 100644
--- a/site/status.json
+++ b/site/status.json
@@ -1,11 +1,11 @@
{
- "version": "0.20.0-beta.7",
+ "version": "0.20.0-beta.8",
"release_state": "released",
"source_state": "release",
- "installer_version": "0.20.0-beta.7",
- "tests": 3437,
+ "installer_version": "0.20.0-beta.8",
+ "tests": 3438,
"redteam_faelle": 277,
"kernel_zeilen": 996,
"werkzeuge": 37,
- "release_ref": "v0.20.0-beta.7"
+ "release_ref": "v0.20.0-beta.8"
}
diff --git a/site/vergleich/index.html b/site/vergleich/index.html
index 2e074b7..b93b978 100644
--- a/site/vergleich/index.html
+++ b/site/vergleich/index.html
@@ -290,7 +290,7 @@
The others can do more.
37tools here, every one gated
100+skills there · counted honestly
-
3437automated tests
+
3438automated tests
277/277adversarial cases
996gate lines
0inbound ports
diff --git a/talos/__init__.py b/talos/__init__.py
index cebe782..cb507f7 100644
--- a/talos/__init__.py
+++ b/talos/__init__.py
@@ -8,4 +8,4 @@
advertised and a released package is never left behind.
"""
-__version__ = "0.20.0-beta.7"
+__version__ = "0.20.0-beta.8"
diff --git a/talos/updater.py b/talos/updater.py
index 71772f8..33ef8ca 100644
--- a/talos/updater.py
+++ b/talos/updater.py
@@ -532,7 +532,7 @@ def _prepare(run: Runner, plan: _Plan, out: _Writer) -> None:
raise UpdateError("venv failed — is python3-venv installed?")
venv_python = plan.staging / VENV_PYTHON
for name, args in dependency_installs(plan.staging):
- argv = [str(venv_python), "-m", "pip", "install", "--quiet", *args]
+ argv = [str(venv_python), "-I", "-m", "pip", "--isolated", "install", "--quiet", *args]
if run(argv, plan.staging) != 0:
raise UpdateError(f"{name} could not be installed. Nothing was switched.")
out.ok(f".venv in {plan.staging.name} — {plan.prefix} untouched so far")
diff --git a/tests/test_requirements_lock.py b/tests/test_requirements_lock.py
index 4792f3c..39b2b85 100644
--- a/tests/test_requirements_lock.py
+++ b/tests/test_requirements_lock.py
@@ -174,6 +174,7 @@ def test_the_installer_installs_from_the_locks_and_upgrades_nothing_unpinned() -
assert '--require-hashes -r "$PREFIX/requirements-dev.lock"' in text
assert "-r \"$PREFIX/requirements.txt\"" not in text
assert "--upgrade pip" not in text
+ assert text.count("-I -m pip --isolated install") == 3
@pytest.mark.skipif(not WORKFLOW.exists(), reason="kein Workflow in diesem Baum")
@@ -186,7 +187,7 @@ def test_ci_installs_from_the_locks() -> None:
def test_verifier_lock_is_a_hashed_subset_of_audited_runtime() -> None:
lock = ROOT / "requirements-verifier.lock"
pins = _pins(lock)
- assert set(pins) == {"cryptography", "cffi", "pycparser"}
+ assert set(pins) == {"cryptography", "cffi", "pycparser", "pip"}
runtime = _pins(RUNTIME[1])
assert all(pins[name] == runtime[name] for name in pins)
assert all(count > 0 for entries in pins.values() for _, count in entries)
@@ -201,3 +202,13 @@ def test_verifier_pins_are_embedded_before_archive_execution() -> None:
assert command in text
assert text.index(command) < text.index('tar -xzf')
assert '--quiet cryptography' not in text
+
+
+def test_python_seeded_pip_is_replaced_by_a_hash_locked_safe_version() -> None:
+ """A clean application lock must not leave the interpreter's vulnerable pip seed."""
+ requirement = next(r for r in _declared(RUNTIME[0]) if _canonical(r.name) == "pip")
+ assert not requirement.specifier.contains(Version("26.1.2"))
+ assert requirement.specifier.contains(Version("26.2.0"))
+ for name in ("requirements.lock", "requirements-verifier.lock"):
+ assert all(version >= Version("26.2.0") and hashes > 0
+ for version, hashes in _pins(ROOT / name)["pip"])
diff --git a/tests/test_updater.py b/tests/test_updater.py
index 050aeb2..5208856 100644
--- a/tests/test_updater.py
+++ b/tests/test_updater.py
@@ -430,6 +430,8 @@ def test_nothing_is_started_and_nothing_is_scheduled(tmp_path: Path) -> None:
# Der ganze Lauf besteht aus venv, pip, pytest, redteam — nichts startet den Agenten.
assert set(runner.steps) == {"venv", "pip", "pytest", "redteam"}
for argv, _ in runner.calls:
+ if "pip" in argv:
+ assert argv[1:6] == ["-I", "-m", "pip", "--isolated", "install"]
# These are subprocess argv, not shell text. An '&' in a path is data.
assert not {"crontab", "systemctl", "launchctl", "nohup", "&"}.intersection(argv)
assert not any(a == "-m" and b == "talos" for a, b in zip(argv, argv[1:]))