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 @@

- - Tests + Tests Red team Gate path Tools @@ -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:]))