neuroncli 7.0.0a4__tar.gz → 7.0.0a5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,7 +18,9 @@ the `neuron` binary), the Python porting workspace under src/ and tests/, and
18
18
  the parity documents were imported from Claw Code and have since been modified.
19
19
  The Claw Code copyright and permission notice is reproduced in LICENSE and must
20
20
  be kept in all copies or substantial portions of the software. See
21
- docs/PROVENANCE.md for the code lineage.
21
+ docs/PROVENANCE.md for the code lineage. Fixes ported from later Claw Code
22
+ commits (fork point about 00d0eb6, reviewed up to 51cfca1) are listed with their
23
+ upstream commit SHAs under "Later ports from Claw Code" in docs/PROVENANCE.md.
22
24
 
23
25
  Full-screen UI (neuron-tui)
24
26
  ---------------------------
@@ -80,12 +82,15 @@ locally with:
80
82
 
81
83
  node scripts/generate-third-party-notices.mjs
82
84
 
83
- npm package dependencies
84
- ------------------------
85
+ Default MCP servers
86
+ -------------------
85
87
 
86
- The npm package @zero-x.live/neuron installs, but does not bundle,
87
- codebase-memory-mcp (MIT License) and @playwright/mcp (Apache License 2.0).
88
- npm installs them with their own license files.
88
+ Neither the npm package @zero-x.live/neuron nor the binary bundles the
89
+ default MCP servers. On first use Neuron downloads @playwright/mcp (Apache
90
+ License 2.0) and its dependencies from the npm registry, with their own
91
+ license files, and the codebase-memory-mcp release binary (MIT License) from
92
+ the project's GitHub release, and checks each against the hash its connector
93
+ catalog pins.
89
94
 
90
95
  Trademarks and affiliation
91
96
  --------------------------
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: neuroncli
3
- Version: 7.0.0a4
3
+ Version: 7.0.0a5
4
4
  Summary: Neuron - terminal coding agent powered by the Zero-X gateway
5
5
  Author: RAHUL S / Zero-X
6
6
  License-Expression: MIT
@@ -36,7 +36,7 @@ By default Neuron talks to models through the [Zero-X gateway](https://zero-x.li
36
36
  You sign in once with `neuron auth login`; the provider API keys stay on the gateway
37
37
  and never reach your machine. You can also use your own provider key instead.
38
38
 
39
- > 7.0.0-alpha.4 is Neuron's fourth alpha, built and published by this repository's
39
+ > 7.0.0-alpha.5 is Neuron's fifth alpha, built and published by this repository's
40
40
  > release pipeline. Its known limitations are listed in
41
41
  > the [changelog](https://github.com/RAHUL-DevelopeRR/neuron-v2/blob/HEAD/CHANGELOG.md).
42
42
  > It is a GitHub prerelease, so the install scripts need the versioned commands below.
@@ -56,25 +56,25 @@ and there is none until the first stable release is out.
56
56
  **macOS and Linux**
57
57
 
58
58
  ```sh
59
- curl -fsSL https://github.com/RAHUL-DevelopeRR/neuron-v2/releases/download/v7.0.0-alpha.4/install.sh | sh -s -- --version 7.0.0-alpha.4
59
+ curl -fsSL https://github.com/RAHUL-DevelopeRR/neuron-v2/releases/download/v7.0.0-alpha.5/install.sh | sh -s -- --version 7.0.0-alpha.5
60
60
  ```
61
61
 
62
62
  The script installs to `~/.local/bin` and tells you if that directory is not on your
63
63
  `PATH`. Pass options after `sh -s --`, for example
64
- `sh -s -- --version 7.0.0-alpha.4 --install-dir "$HOME/bin"`, or set `NEURON_VERSION`
64
+ `sh -s -- --version 7.0.0-alpha.5 --install-dir "$HOME/bin"`, or set `NEURON_VERSION`
65
65
  and `NEURON_INSTALL_DIR`.
66
66
 
67
67
  **Windows (PowerShell 5.1 or 7)**
68
68
 
69
69
  ```powershell
70
- & ([scriptblock]::Create((irm https://github.com/RAHUL-DevelopeRR/neuron-v2/releases/download/v7.0.0-alpha.4/install.ps1))) -Version 7.0.0-alpha.4
70
+ & ([scriptblock]::Create((irm https://github.com/RAHUL-DevelopeRR/neuron-v2/releases/download/v7.0.0-alpha.5/install.ps1))) -Version 7.0.0-alpha.5
71
71
  ```
72
72
 
73
73
  The script installs to `%LOCALAPPDATA%\Programs\Neuron\bin` and only changes your
74
74
  `PATH` if you ask it to:
75
75
 
76
76
  ```powershell
77
- & ([scriptblock]::Create((irm https://github.com/RAHUL-DevelopeRR/neuron-v2/releases/download/v7.0.0-alpha.4/install.ps1))) -Version 7.0.0-alpha.4 -AddToPath
77
+ & ([scriptblock]::Create((irm https://github.com/RAHUL-DevelopeRR/neuron-v2/releases/download/v7.0.0-alpha.5/install.ps1))) -Version 7.0.0-alpha.5 -AddToPath
78
78
  ```
79
79
 
80
80
  **npm** (Node.js 22.14 or later)
@@ -91,9 +91,9 @@ pip install --pre neuroncli
91
91
  ```
92
92
 
93
93
  The registry packages are published after the GitHub release, so check them first:
94
- `npm view @zero-x.live/neuron version` should print `7.0.0-alpha.4`, and
95
- `pip index versions neuroncli --pre` should list `7.0.0a4`. If they still show the
96
- previous alpha (`7.0.0-alpha.3`, or `7.0.0a3` on PyPI), use the install scripts above
94
+ `npm view @zero-x.live/neuron version` should print `7.0.0-alpha.5`, and
95
+ `pip index versions neuroncli --pre` should list `7.0.0a5`. If they still show the
96
+ previous alpha (`7.0.0-alpha.4`, or `7.0.0a4` on PyPI), use the install scripts above
97
97
  for this one. 6.2.0 (npm) and 6.2.4 (PyPI) are old, unsupported builds.
98
98
 
99
99
  The npm and pip packages are small launchers that download the binaries for the
@@ -108,16 +108,18 @@ uses a binary from that cache when its `bin/native` folder has none, and downloa
108
108
  the cache only when that folder is read-only. After that, Neuron starts without network
109
109
  access. Ctrl+C goes to Neuron, and the launcher exits with Neuron's exit code.
110
110
 
111
- With the installers or pip, the two default MCP servers (CodebaseMap, which `/mcp`
112
- lists as `codebase-memory`, and Playwright) start through `npx -y`, so they need
113
- Node.js; the npm package installs them with Neuron.
111
+ The two default MCP servers (CodebaseMap, which `/mcp` lists as `codebase-memory`,
112
+ and Playwright) never start through `npx -y`: on first use Neuron installs each into
113
+ `~/.neuron/mcp` from downloads whose hashes its connector catalog pins, and later
114
+ starts are offline. Playwright needs Node.js (the npm launcher passes its own).
114
115
 
115
116
  On a terminal, `neuron` with no prompt opens the full-screen UI, `neuron-tui`. The
116
117
  installers put it beside `neuron`, and the launchers fetch it the same way, verified
117
118
  by checksum. `neuron --no-tui` (or `NEURON_NO_TUI=1`) keeps the line REPL. Without
118
119
  the UI, for example after a failed download, `neuron` runs the line REPL as
119
- before. Prompts (`neuron "fix the test"`, `neuron -p ...`) and subcommands never
120
- start it.
120
+ before. On a terminal, `neuron "fix the test"` opens it with that prompt and
121
+ `neuron --resume SESSION` on that session; `neuron -p ...`, `neuron prompt ...` and
122
+ subcommands never start it.
121
123
 
122
124
  **From source**
123
125
 
@@ -10,7 +10,7 @@ By default Neuron talks to models through the [Zero-X gateway](https://zero-x.li
10
10
  You sign in once with `neuron auth login`; the provider API keys stay on the gateway
11
11
  and never reach your machine. You can also use your own provider key instead.
12
12
 
13
- > 7.0.0-alpha.4 is Neuron's fourth alpha, built and published by this repository's
13
+ > 7.0.0-alpha.5 is Neuron's fifth alpha, built and published by this repository's
14
14
  > release pipeline. Its known limitations are listed in
15
15
  > the [changelog](https://github.com/RAHUL-DevelopeRR/neuron-v2/blob/HEAD/CHANGELOG.md).
16
16
  > It is a GitHub prerelease, so the install scripts need the versioned commands below.
@@ -30,25 +30,25 @@ and there is none until the first stable release is out.
30
30
  **macOS and Linux**
31
31
 
32
32
  ```sh
33
- curl -fsSL https://github.com/RAHUL-DevelopeRR/neuron-v2/releases/download/v7.0.0-alpha.4/install.sh | sh -s -- --version 7.0.0-alpha.4
33
+ curl -fsSL https://github.com/RAHUL-DevelopeRR/neuron-v2/releases/download/v7.0.0-alpha.5/install.sh | sh -s -- --version 7.0.0-alpha.5
34
34
  ```
35
35
 
36
36
  The script installs to `~/.local/bin` and tells you if that directory is not on your
37
37
  `PATH`. Pass options after `sh -s --`, for example
38
- `sh -s -- --version 7.0.0-alpha.4 --install-dir "$HOME/bin"`, or set `NEURON_VERSION`
38
+ `sh -s -- --version 7.0.0-alpha.5 --install-dir "$HOME/bin"`, or set `NEURON_VERSION`
39
39
  and `NEURON_INSTALL_DIR`.
40
40
 
41
41
  **Windows (PowerShell 5.1 or 7)**
42
42
 
43
43
  ```powershell
44
- & ([scriptblock]::Create((irm https://github.com/RAHUL-DevelopeRR/neuron-v2/releases/download/v7.0.0-alpha.4/install.ps1))) -Version 7.0.0-alpha.4
44
+ & ([scriptblock]::Create((irm https://github.com/RAHUL-DevelopeRR/neuron-v2/releases/download/v7.0.0-alpha.5/install.ps1))) -Version 7.0.0-alpha.5
45
45
  ```
46
46
 
47
47
  The script installs to `%LOCALAPPDATA%\Programs\Neuron\bin` and only changes your
48
48
  `PATH` if you ask it to:
49
49
 
50
50
  ```powershell
51
- & ([scriptblock]::Create((irm https://github.com/RAHUL-DevelopeRR/neuron-v2/releases/download/v7.0.0-alpha.4/install.ps1))) -Version 7.0.0-alpha.4 -AddToPath
51
+ & ([scriptblock]::Create((irm https://github.com/RAHUL-DevelopeRR/neuron-v2/releases/download/v7.0.0-alpha.5/install.ps1))) -Version 7.0.0-alpha.5 -AddToPath
52
52
  ```
53
53
 
54
54
  **npm** (Node.js 22.14 or later)
@@ -65,9 +65,9 @@ pip install --pre neuroncli
65
65
  ```
66
66
 
67
67
  The registry packages are published after the GitHub release, so check them first:
68
- `npm view @zero-x.live/neuron version` should print `7.0.0-alpha.4`, and
69
- `pip index versions neuroncli --pre` should list `7.0.0a4`. If they still show the
70
- previous alpha (`7.0.0-alpha.3`, or `7.0.0a3` on PyPI), use the install scripts above
68
+ `npm view @zero-x.live/neuron version` should print `7.0.0-alpha.5`, and
69
+ `pip index versions neuroncli --pre` should list `7.0.0a5`. If they still show the
70
+ previous alpha (`7.0.0-alpha.4`, or `7.0.0a4` on PyPI), use the install scripts above
71
71
  for this one. 6.2.0 (npm) and 6.2.4 (PyPI) are old, unsupported builds.
72
72
 
73
73
  The npm and pip packages are small launchers that download the binaries for the
@@ -82,16 +82,18 @@ uses a binary from that cache when its `bin/native` folder has none, and downloa
82
82
  the cache only when that folder is read-only. After that, Neuron starts without network
83
83
  access. Ctrl+C goes to Neuron, and the launcher exits with Neuron's exit code.
84
84
 
85
- With the installers or pip, the two default MCP servers (CodebaseMap, which `/mcp`
86
- lists as `codebase-memory`, and Playwright) start through `npx -y`, so they need
87
- Node.js; the npm package installs them with Neuron.
85
+ The two default MCP servers (CodebaseMap, which `/mcp` lists as `codebase-memory`,
86
+ and Playwright) never start through `npx -y`: on first use Neuron installs each into
87
+ `~/.neuron/mcp` from downloads whose hashes its connector catalog pins, and later
88
+ starts are offline. Playwright needs Node.js (the npm launcher passes its own).
88
89
 
89
90
  On a terminal, `neuron` with no prompt opens the full-screen UI, `neuron-tui`. The
90
91
  installers put it beside `neuron`, and the launchers fetch it the same way, verified
91
92
  by checksum. `neuron --no-tui` (or `NEURON_NO_TUI=1`) keeps the line REPL. Without
92
93
  the UI, for example after a failed download, `neuron` runs the line REPL as
93
- before. Prompts (`neuron "fix the test"`, `neuron -p ...`) and subcommands never
94
- start it.
94
+ before. On a terminal, `neuron "fix the test"` opens it with that prompt and
95
+ `neuron --resume SESSION` on that session; `neuron -p ...`, `neuron prompt ...` and
96
+ subcommands never start it.
95
97
 
96
98
  **From source**
97
99
 
@@ -1,9 +1,11 @@
1
1
  """NeuronCLI - Python launcher for the native Neuron binary.
2
2
 
3
3
  The first launch downloads the release binary that matches this package
4
- version, verifies it against the release's SHA-256 file and caches it together
5
- with a ``.sha256`` sidecar. Later launches run the cached binary without any
6
- network access.
4
+ version, verifies it against the release's SHA-256 file *and* the release's
5
+ ``SHA256SUMS`` and caches it together with a ``.sha256`` sidecar and a
6
+ ``.verified`` stamp (size and mtime). Later launches run the cached binary
7
+ without any network access; a binary whose size or mtime changed since it was
8
+ verified is hashed again and refused when it no longer matches its sidecar.
7
9
 
8
10
  On a terminal, a bare ``neuron`` opens Neuron's full-screen UI: the launcher
9
11
  gets ``neuron-tui`` the same way (once, verified and cached) and tells
@@ -14,6 +16,8 @@ Environment:
14
16
  NEURON_BINARY_PATH run this native binary instead (development)
15
17
  NEURON_CACHE_DIR cache root (default: the platform user cache)
16
18
  NEURON_DOWNLOAD_BASE release download base URL (HTTPS, or loopback HTTP)
19
+ NEURON_CHECKSUMS_BASE where SHA256SUMS comes from (default: the GitHub
20
+ release, even when a mirror serves the binary)
17
21
  NEURON_NO_TUI 1 keeps the line REPL and skips the UI download
18
22
  NEURON_TUI_BIN the full-screen UI to use (passed on unchanged)
19
23
  """
@@ -34,7 +38,7 @@ from urllib.error import HTTPError, URLError
34
38
  from urllib.parse import urlsplit
35
39
  from urllib.request import urlopen
36
40
 
37
- PACKAGE_VERSION = "7.0.0-alpha.4"
41
+ PACKAGE_VERSION = "7.0.0-alpha.5"
38
42
  REPOSITORY = "https://github.com/RAHUL-DevelopeRR/neuron-v2"
39
43
  DEFAULT_DOWNLOAD_BASE = f"{REPOSITORY}/releases/download"
40
44
  CHECKSUM_TIMEOUT_SECONDS = 30
@@ -118,6 +122,30 @@ def _download_base() -> str:
118
122
  raise LauncherError(f"NEURON_DOWNLOAD_BASE must be an HTTPS URL (or loopback HTTP): {base}")
119
123
 
120
124
 
125
+ def _checksums_base() -> str:
126
+ """Where SHA256SUMS comes from: the GitHub release unless NEURON_CHECKSUMS_BASE says otherwise."""
127
+ base = (os.environ.get("NEURON_CHECKSUMS_BASE") or DEFAULT_DOWNLOAD_BASE).rstrip("/")
128
+ parts = urlsplit(base)
129
+ if parts.scheme == "https" or (parts.scheme == "http" and _is_loopback(parts.hostname or "")):
130
+ return base
131
+ raise LauncherError(f"NEURON_CHECKSUMS_BASE must be an HTTPS URL (or loopback HTTP): {base}")
132
+
133
+
134
+ def _parse_sums(text: str, asset: str) -> Optional[str]:
135
+ """The hash listed for ``asset`` in a SHA256SUMS file (``<hash> <name>`` or ``<hash> *<name>``)."""
136
+ for line in text.splitlines():
137
+ fields = line.strip().split(None, 1)
138
+ if len(fields) != 2:
139
+ continue
140
+ name = fields[1].strip()
141
+ if name.startswith("*"):
142
+ name = name[1:]
143
+ digest = _parse_checksum(fields[0])
144
+ if digest is not None and name == asset:
145
+ return digest
146
+ return None
147
+
148
+
121
149
  def _digest(path: Path) -> str:
122
150
  checksum = hashlib.sha256()
123
151
  with path.open("rb") as stream:
@@ -140,17 +168,53 @@ def _sidecar(binary: Path) -> Path:
140
168
  return binary.with_name(binary.name + ".sha256")
141
169
 
142
170
 
171
+ def _stamp_path(binary: Path) -> Path:
172
+ return binary.with_name(binary.name + ".verified")
173
+
174
+
175
+ def _stamp_of(binary: Path) -> str:
176
+ info = binary.stat()
177
+ return f"{info.st_size} {info.st_mtime_ns}\n"
178
+
179
+
180
+ def _write_stamp(binary: Path) -> None:
181
+ try:
182
+ _stamp_path(binary).write_text(_stamp_of(binary), encoding="ascii")
183
+ except OSError:
184
+ pass # a read-only cache only costs a re-hash on the next launch
185
+
186
+
143
187
  def _cached_binary(cache: Path, asset: str) -> Optional[Path]:
144
- """The cached binary when it was verified at download time (sidecar present)."""
188
+ """The cached binary when it was verified at download time (sidecar present).
189
+
190
+ Its size and mtime are compared with the ``.verified`` stamp; when they differ the file is
191
+ hashed again, and a hash that no longer matches the sidecar raises instead of running it.
192
+ """
145
193
  binary = cache / asset
146
194
  sidecar = _sidecar(binary)
147
195
  if not binary.is_file() or not sidecar.is_file():
148
196
  return None
149
197
  try:
150
- if _parse_checksum(sidecar.read_text(encoding="ascii", errors="replace")) is None:
151
- return None
198
+ expected = _parse_checksum(sidecar.read_text(encoding="ascii", errors="replace"))
152
199
  except OSError:
153
200
  return None
201
+ if expected is None:
202
+ return None
203
+ try:
204
+ try:
205
+ if _stamp_path(binary).read_text(encoding="ascii") == _stamp_of(binary):
206
+ return binary
207
+ except OSError:
208
+ pass # no stamp yet: hash below
209
+ matches = _digest(binary) == expected
210
+ except OSError:
211
+ matches = False
212
+ if not matches:
213
+ raise LauncherError(
214
+ f"{binary} does not match its recorded SHA-256 ({sidecar}), so it was not run. Delete "
215
+ f"{binary}, {sidecar} and {_stamp_path(binary)} to download a fresh, verified copy."
216
+ )
217
+ _write_stamp(binary)
154
218
  return binary
155
219
 
156
220
 
@@ -186,6 +250,7 @@ def _download_binary(version: str, asset: str, cache: Path, label: str = "Neuron
186
250
  base = f"{_download_base()}/v{version}"
187
251
  binary_url = f"{base}/{asset}"
188
252
  checksum_url = f"{binary_url}.sha256"
253
+ sums_url = f"{_checksums_base()}/v{version}/SHA256SUMS"
189
254
  destination = cache / asset
190
255
  try:
191
256
  cache.mkdir(parents=True, exist_ok=True)
@@ -193,6 +258,16 @@ def _download_binary(version: str, asset: str, cache: Path, label: str = "Neuron
193
258
  raise LauncherError(f"Cannot create the Neuron cache directory {cache}: {error}. Set NEURON_CACHE_DIR.") from error
194
259
 
195
260
  print(f"Downloading {label} {version} ({asset}) to {cache} ...", file=sys.stderr, flush=True)
261
+ try:
262
+ with urlopen(sums_url, timeout=CHECKSUM_TIMEOUT_SECONDS) as response:
263
+ sums_text = response.read(1024 * 1024).decode("utf-8", errors="replace")
264
+ except (HTTPError, URLError, OSError) as error:
265
+ raise _network_error(sums_url, version, cache, error) from error
266
+ listed = _parse_sums(sums_text, asset)
267
+ if listed is None:
268
+ raise LauncherError(
269
+ f"The release SHA256SUMS ({sums_url}) is empty or does not list {asset}. Nothing was cached."
270
+ )
196
271
  try:
197
272
  with urlopen(checksum_url, timeout=CHECKSUM_TIMEOUT_SECONDS) as response:
198
273
  checksum_text = response.read(4096).decode("ascii", errors="replace")
@@ -225,6 +300,11 @@ def _download_binary(version: str, asset: str, cache: Path, label: str = "Neuron
225
300
  f"Neuron {version} ({asset}) failed its SHA-256 check: expected {expected}, got {actual}. "
226
301
  "The download was discarded; nothing was cached."
227
302
  )
303
+ if actual != listed:
304
+ raise LauncherError(
305
+ f"Neuron {version} ({asset}) is not what the release SHA256SUMS ({sums_url}) lists: it lists "
306
+ f"{listed}, got {actual}. The download was discarded; nothing was cached."
307
+ )
228
308
  if os.name != "nt":
229
309
  temporary.chmod(0o755)
230
310
 
@@ -235,6 +315,7 @@ def _download_binary(version: str, asset: str, cache: Path, label: str = "Neuron
235
315
  _sidecar(destination),
236
316
  lambda temporary: temporary.write_text(f"{expected} {asset}\n", encoding="ascii"),
237
317
  )
318
+ _write_stamp(destination)
238
319
  except OSError as error:
239
320
  raise LauncherError(
240
321
  f"Cannot write Neuron to {cache}: {error}. Free some disk space or set NEURON_CACHE_DIR."
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: neuroncli
3
- Version: 7.0.0a4
3
+ Version: 7.0.0a5
4
4
  Summary: Neuron - terminal coding agent powered by the Zero-X gateway
5
5
  Author: RAHUL S / Zero-X
6
6
  License-Expression: MIT
@@ -36,7 +36,7 @@ By default Neuron talks to models through the [Zero-X gateway](https://zero-x.li
36
36
  You sign in once with `neuron auth login`; the provider API keys stay on the gateway
37
37
  and never reach your machine. You can also use your own provider key instead.
38
38
 
39
- > 7.0.0-alpha.4 is Neuron's fourth alpha, built and published by this repository's
39
+ > 7.0.0-alpha.5 is Neuron's fifth alpha, built and published by this repository's
40
40
  > release pipeline. Its known limitations are listed in
41
41
  > the [changelog](https://github.com/RAHUL-DevelopeRR/neuron-v2/blob/HEAD/CHANGELOG.md).
42
42
  > It is a GitHub prerelease, so the install scripts need the versioned commands below.
@@ -56,25 +56,25 @@ and there is none until the first stable release is out.
56
56
  **macOS and Linux**
57
57
 
58
58
  ```sh
59
- curl -fsSL https://github.com/RAHUL-DevelopeRR/neuron-v2/releases/download/v7.0.0-alpha.4/install.sh | sh -s -- --version 7.0.0-alpha.4
59
+ curl -fsSL https://github.com/RAHUL-DevelopeRR/neuron-v2/releases/download/v7.0.0-alpha.5/install.sh | sh -s -- --version 7.0.0-alpha.5
60
60
  ```
61
61
 
62
62
  The script installs to `~/.local/bin` and tells you if that directory is not on your
63
63
  `PATH`. Pass options after `sh -s --`, for example
64
- `sh -s -- --version 7.0.0-alpha.4 --install-dir "$HOME/bin"`, or set `NEURON_VERSION`
64
+ `sh -s -- --version 7.0.0-alpha.5 --install-dir "$HOME/bin"`, or set `NEURON_VERSION`
65
65
  and `NEURON_INSTALL_DIR`.
66
66
 
67
67
  **Windows (PowerShell 5.1 or 7)**
68
68
 
69
69
  ```powershell
70
- & ([scriptblock]::Create((irm https://github.com/RAHUL-DevelopeRR/neuron-v2/releases/download/v7.0.0-alpha.4/install.ps1))) -Version 7.0.0-alpha.4
70
+ & ([scriptblock]::Create((irm https://github.com/RAHUL-DevelopeRR/neuron-v2/releases/download/v7.0.0-alpha.5/install.ps1))) -Version 7.0.0-alpha.5
71
71
  ```
72
72
 
73
73
  The script installs to `%LOCALAPPDATA%\Programs\Neuron\bin` and only changes your
74
74
  `PATH` if you ask it to:
75
75
 
76
76
  ```powershell
77
- & ([scriptblock]::Create((irm https://github.com/RAHUL-DevelopeRR/neuron-v2/releases/download/v7.0.0-alpha.4/install.ps1))) -Version 7.0.0-alpha.4 -AddToPath
77
+ & ([scriptblock]::Create((irm https://github.com/RAHUL-DevelopeRR/neuron-v2/releases/download/v7.0.0-alpha.5/install.ps1))) -Version 7.0.0-alpha.5 -AddToPath
78
78
  ```
79
79
 
80
80
  **npm** (Node.js 22.14 or later)
@@ -91,9 +91,9 @@ pip install --pre neuroncli
91
91
  ```
92
92
 
93
93
  The registry packages are published after the GitHub release, so check them first:
94
- `npm view @zero-x.live/neuron version` should print `7.0.0-alpha.4`, and
95
- `pip index versions neuroncli --pre` should list `7.0.0a4`. If they still show the
96
- previous alpha (`7.0.0-alpha.3`, or `7.0.0a3` on PyPI), use the install scripts above
94
+ `npm view @zero-x.live/neuron version` should print `7.0.0-alpha.5`, and
95
+ `pip index versions neuroncli --pre` should list `7.0.0a5`. If they still show the
96
+ previous alpha (`7.0.0-alpha.4`, or `7.0.0a4` on PyPI), use the install scripts above
97
97
  for this one. 6.2.0 (npm) and 6.2.4 (PyPI) are old, unsupported builds.
98
98
 
99
99
  The npm and pip packages are small launchers that download the binaries for the
@@ -108,16 +108,18 @@ uses a binary from that cache when its `bin/native` folder has none, and downloa
108
108
  the cache only when that folder is read-only. After that, Neuron starts without network
109
109
  access. Ctrl+C goes to Neuron, and the launcher exits with Neuron's exit code.
110
110
 
111
- With the installers or pip, the two default MCP servers (CodebaseMap, which `/mcp`
112
- lists as `codebase-memory`, and Playwright) start through `npx -y`, so they need
113
- Node.js; the npm package installs them with Neuron.
111
+ The two default MCP servers (CodebaseMap, which `/mcp` lists as `codebase-memory`,
112
+ and Playwright) never start through `npx -y`: on first use Neuron installs each into
113
+ `~/.neuron/mcp` from downloads whose hashes its connector catalog pins, and later
114
+ starts are offline. Playwright needs Node.js (the npm launcher passes its own).
114
115
 
115
116
  On a terminal, `neuron` with no prompt opens the full-screen UI, `neuron-tui`. The
116
117
  installers put it beside `neuron`, and the launchers fetch it the same way, verified
117
118
  by checksum. `neuron --no-tui` (or `NEURON_NO_TUI=1`) keeps the line REPL. Without
118
119
  the UI, for example after a failed download, `neuron` runs the line REPL as
119
- before. Prompts (`neuron "fix the test"`, `neuron -p ...`) and subcommands never
120
- start it.
120
+ before. On a terminal, `neuron "fix the test"` opens it with that prompt and
121
+ `neuron --resume SESSION` on that session; `neuron -p ...`, `neuron prompt ...` and
122
+ subcommands never start it.
121
123
 
122
124
  **From source**
123
125
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "neuroncli"
7
- version = "7.0.0-alpha.4"
7
+ version = "7.0.0-alpha.5"
8
8
  description = "Neuron - terminal coding agent powered by the Zero-X gateway"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -126,6 +126,7 @@ class InstallShTests(unittest.TestCase):
126
126
  environment.update(
127
127
  HOME=shell_path(self.root / "home"),
128
128
  NEURON_DOWNLOAD_BASE=self.server.base_url,
129
+ NEURON_CHECKSUMS_BASE=self.server.base_url,
129
130
  NEURON_RELEASES_API=f"{self.server.base_url}/repos/neuron/releases/latest",
130
131
  NEURON_INSTALL_DIR=shell_path(self.install_dir),
131
132
  # The full-screen UI step has its own tests; NEURON_NO_TUI="" turns it on.
@@ -188,7 +189,7 @@ class InstallShTests(unittest.TestCase):
188
189
  self.assertIn("is not on your PATH", result.stderr)
189
190
  self.assertEqual(
190
191
  self.server.paths(),
191
- [f"/v{VERSION}/neuron-linux-x64.sha256", f"/v{VERSION}/neuron-linux-x64"],
192
+ [f"/v{VERSION}/SHA256SUMS", f"/v{VERSION}/neuron-linux-x64.sha256", f"/v{VERSION}/neuron-linux-x64"],
192
193
  )
193
194
  self.assertEqual(self.installed().read_bytes(), posix_dummy(VERSION))
194
195
  self.assertIn(f"neuron-dummy {VERSION}", self.run_installed("--version"))
@@ -204,6 +205,7 @@ class InstallShTests(unittest.TestCase):
204
205
  self.assertEqual(
205
206
  self.server.paths(),
206
207
  [
208
+ f"/v{VERSION}/SHA256SUMS",
207
209
  f"/v{VERSION}/neuron-linux-x64.sha256",
208
210
  f"/v{VERSION}/neuron-linux-x64",
209
211
  f"/v{VERSION}/neuron-tui-linux-x64.sha256",
@@ -295,6 +297,32 @@ class InstallShTests(unittest.TestCase):
295
297
  self.assertIn("Nothing was installed", result.stderr)
296
298
  self.assertEqual(self.installed().read_bytes(), b"previous neuron")
297
299
 
300
+ def test_release_sums_that_disagree_or_are_missing_fail_closed(self) -> None:
301
+ fake_release.write_executable(self.installed(), b"previous neuron")
302
+ fake_release.stage_release(self.releases, VERSION, "neuron-linux-x64", posix_dummy(VERSION))
303
+ sums = self.releases / f"v{VERSION}" / "SHA256SUMS"
304
+ sums.write_text(f"{'c' * 64} neuron-linux-x64\n", encoding="ascii")
305
+ wrong = self.run_installer(NEURON_VERSION=VERSION)
306
+ self.assertEqual(wrong.returncode, 1)
307
+ self.assertIn("SHA256SUMS", wrong.stderr)
308
+ self.assertIn("Nothing was installed", wrong.stderr)
309
+ sums.write_text(f"{'c' * 64} some-other-asset\n", encoding="ascii")
310
+ unlisted = self.run_installer(NEURON_VERSION=VERSION)
311
+ self.assertEqual(unlisted.returncode, 1)
312
+ sums.unlink()
313
+ missing = self.run_installer(NEURON_VERSION=VERSION)
314
+ self.assertEqual(missing.returncode, 1)
315
+ self.assertEqual(self.installed().read_bytes(), b"previous neuron")
316
+
317
+ def test_checksums_come_from_their_own_base_not_the_mirror(self) -> None:
318
+ fake_release.stage_release(self.releases, VERSION, "neuron-linux-x64", posix_dummy(VERSION))
319
+ result = self.run_installer(NEURON_VERSION=VERSION, NEURON_CHECKSUMS_BASE=fake_release.unused_loopback_url())
320
+ self.assertEqual(result.returncode, 1)
321
+ self.assertNotIn(f"/v{VERSION}/SHA256SUMS", self.server.paths())
322
+ insecure = self.run_installer(NEURON_VERSION=VERSION, NEURON_CHECKSUMS_BASE="http://downloads.example.com")
323
+ self.assertEqual(insecure.returncode, 1)
324
+ self.assertIn("NEURON_CHECKSUMS_BASE must be an HTTPS URL", insecure.stderr)
325
+
298
326
  def test_malformed_checksum_file_is_rejected(self) -> None:
299
327
  fake_release.stage_release(self.releases, VERSION, "neuron-linux-x64", posix_dummy(VERSION), checksum="oops\n")
300
328
  result = self.run_installer(NEURON_VERSION=VERSION)
@@ -316,6 +344,18 @@ class InstallShTests(unittest.TestCase):
316
344
  self.assertIn("Next steps:", result.stderr)
317
345
  self.assertEqual(self.installed().read_bytes(), STDIN_READING_BINARY)
318
346
 
347
+ def test_every_curl_call_refuses_a_redirect_away_from_https(self) -> None:
348
+ # A mirror or release redirecting to plain HTTP must not be followed (--proto-redir
349
+ # only limits redirects; a loopback http base given on purpose still works).
350
+ calls = [
351
+ line.strip()
352
+ for line in INSTALL_SH.read_text(encoding="utf-8").splitlines()
353
+ if line.lstrip().startswith("curl ") and "--fail" in line
354
+ ]
355
+ self.assertGreaterEqual(len(calls), 2, calls)
356
+ for call in calls:
357
+ self.assertIn("--proto-redir =https", call)
358
+
319
359
  def test_unpublished_version_points_at_the_releases_page(self) -> None:
320
360
  result = self.run_installer(NEURON_VERSION="9.9.9")
321
361
  self.assertEqual(result.returncode, 1)
@@ -323,7 +363,7 @@ class InstallShTests(unittest.TestCase):
323
363
  self.assertIn("releases", result.stderr)
324
364
 
325
365
  def test_network_failure_is_not_reported_as_a_missing_release(self) -> None:
326
- result = self.run_installer(NEURON_VERSION=VERSION, NEURON_DOWNLOAD_BASE=fake_release.unused_loopback_url())
366
+ result = self.run_installer(NEURON_VERSION=VERSION, NEURON_DOWNLOAD_BASE=fake_release.unused_loopback_url(), NEURON_CHECKSUMS_BASE=fake_release.unused_loopback_url())
327
367
  self.assertEqual(result.returncode, 1)
328
368
  self.assertIn("Check your network", result.stderr)
329
369
  self.assertNotIn("may not be published", result.stderr)
@@ -364,15 +404,16 @@ class InstallShTests(unittest.TestCase):
364
404
  self.assertIn("https://api.github.com/repos/RAHUL-DevelopeRR/neuron-v2/releases/latest", calls[0])
365
405
  self.assertNotIn(GITHUB_TOKEN, calls[0])
366
406
  self.assertEqual(stdin_log.read_text(encoding="utf-8"), f'header = "Authorization: Bearer {GITHUB_TOKEN}"\n')
367
- self.assertEqual(self.server.paths(), [f"/v{VERSION}/neuron-linux-x64.sha256", f"/v{VERSION}/neuron-linux-x64"])
368
- self.assertEqual(self.server.authorization, [None, None])
407
+ self.assertEqual(self.server.paths(), [f"/v{VERSION}/SHA256SUMS", f"/v{VERSION}/neuron-linux-x64.sha256", f"/v{VERSION}/neuron-linux-x64"])
408
+ self.assertEqual(self.server.authorization, [None, None, None])
369
409
 
370
410
  def test_github_token_is_not_sent_to_another_releases_api(self) -> None:
371
411
  fake_release.stage_release(self.releases, VERSION, "neuron-linux-x64", posix_dummy(VERSION))
372
412
  result = self.run_installer(GITHUB_TOKEN=GITHUB_TOKEN)
373
413
  self.assertEqual(result.returncode, 0, result.stderr)
374
414
  self.assertEqual(self.server.paths()[0], "/repos/neuron/releases/latest")
375
- self.assertEqual(self.server.authorization, [None, None, None])
415
+ # The releases lookup, then SHA256SUMS, the .sha256 file and the binary: none carries the token.
416
+ self.assertEqual(self.server.authorization, [None, None, None, None])
376
417
 
377
418
  def test_self_check_runs_the_binary_from_the_install_directory(self) -> None:
378
419
  # The temporary directory may be mounted noexec; the binary has to run from the install directory anyway.
@@ -470,6 +511,7 @@ class InstallPs1Tests(unittest.TestCase):
470
511
  environment = clean_environment()
471
512
  environment.update(
472
513
  NEURON_DOWNLOAD_BASE=self.server.base_url,
514
+ NEURON_CHECKSUMS_BASE=self.server.base_url,
473
515
  NEURON_RELEASES_API=f"{self.server.base_url}/repos/neuron/releases/latest",
474
516
  LOCALAPPDATA=str(self.root),
475
517
  # The full-screen UI step has its own tests; NEURON_NO_TUI="" turns it on.
@@ -517,7 +559,8 @@ class InstallPs1Tests(unittest.TestCase):
517
559
  self.assertIn("auth login", result.stdout)
518
560
  self.assertIn("is not on your PATH", result.stdout)
519
561
  self.assertEqual(
520
- self.server.paths(), [f"/v{VERSION}/{self.ASSET}.sha256", f"/v{VERSION}/{self.ASSET}"]
562
+ self.server.paths(),
563
+ [f"/v{VERSION}/SHA256SUMS", f"/v{VERSION}/{self.ASSET}.sha256", f"/v{VERSION}/{self.ASSET}"],
521
564
  )
522
565
  output = subprocess.run([str(self.installed()), "--version"], capture_output=True, text=True, check=True)
523
566
  self.assertIn(f"neuron-dummy {VERSION}", output.stdout)
@@ -557,6 +600,19 @@ class InstallPs1Tests(unittest.TestCase):
557
600
  self.assertEqual(pinned.returncode, 0, pinned.stdout + pinned.stderr)
558
601
  self.assertIn("Installing Neuron 6.3.0", pinned.stdout)
559
602
 
603
+ def test_release_sums_that_disagree_or_are_missing_fail_closed(self) -> None:
604
+ fake_release.stage_release(self.releases, VERSION, self.ASSET)
605
+ sums = self.releases / f"v{VERSION}" / "SHA256SUMS"
606
+ sums.write_text(f"{'d' * 64} {self.ASSET}\n", encoding="ascii")
607
+ wrong = self.run_installer("-Version", VERSION)
608
+ self.assertEqual(wrong.returncode, 1)
609
+ self.assertIn("SHA256SUMS", wrong.stdout + wrong.stderr)
610
+ self.assertFalse(self.installed().exists())
611
+ sums.unlink()
612
+ missing = self.run_installer("-Version", VERSION)
613
+ self.assertEqual(missing.returncode, 1)
614
+ self.assertFalse(self.installed().exists())
615
+
560
616
  def test_checksum_mismatch_installs_nothing(self) -> None:
561
617
  fake_release.stage_release(self.releases, VERSION, self.ASSET, checksum=f"{'b' * 64} {self.ASSET}\n")
562
618
  result = self.run_installer("-Version", VERSION)
@@ -573,7 +629,7 @@ class InstallPs1Tests(unittest.TestCase):
573
629
  self.assertIn("must be an HTTPS URL", insecure.stdout)
574
630
 
575
631
  def test_network_failure_is_not_reported_as_a_missing_release(self) -> None:
576
- result = self.run_installer("-Version", VERSION, NEURON_DOWNLOAD_BASE=fake_release.unused_loopback_url())
632
+ result = self.run_installer("-Version", VERSION, NEURON_DOWNLOAD_BASE=fake_release.unused_loopback_url(), NEURON_CHECKSUMS_BASE=fake_release.unused_loopback_url())
577
633
  self.assertEqual(result.returncode, 1)
578
634
  self.assertIn("Check your network", result.stdout)
579
635
  self.assertNotIn("may not be published", result.stdout)
@@ -591,7 +647,7 @@ class InstallPs1Tests(unittest.TestCase):
591
647
  result = self.run_installer(GITHUB_TOKEN=GITHUB_TOKEN)
592
648
  self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
593
649
  self.assertEqual(self.server.paths()[0], "/repos/neuron/releases/latest")
594
- self.assertEqual(self.server.authorization, [None, None, None])
650
+ self.assertEqual(self.server.authorization, [None, None, None, None])
595
651
 
596
652
  def test_github_token_is_meant_only_for_the_github_api(self) -> None:
597
653
  cases = {
@@ -760,6 +816,7 @@ foreach ($name in @({quoted})) {{
760
816
  "Get-HttpStatus",
761
817
  "Get-NeuronDownload",
762
818
  "Get-NeuronSha256",
819
+ "Test-NeuronSums",
763
820
  "Get-PosixInstallCommand",
764
821
  "Assert-SupportedPlatform",
765
822
  "Resolve-NeuronVersion",
@@ -78,7 +78,7 @@ class NpmTarballTests(unittest.TestCase):
78
78
  self.cache = self.work / "cache"
79
79
 
80
80
  def neuron_env(self, base: str, **extra: str) -> Dict[str, str]:
81
- return self.base_env(NEURON_DOWNLOAD_BASE=base, NEURON_CACHE_DIR=str(self.cache), **extra)
81
+ return self.base_env(NEURON_DOWNLOAD_BASE=base, NEURON_CHECKSUMS_BASE=base, NEURON_CACHE_DIR=str(self.cache), **extra)
82
82
 
83
83
  def install(self, *flags: str) -> Path:
84
84
  result = npm("install", "--global", "--prefix", str(self.prefix), *flags, str(self.tarball), cwd=self.work, env=self.base_env())
@@ -105,7 +105,7 @@ class NpmTarballTests(unittest.TestCase):
105
105
  self.assertIn(f"neuron-dummy {VERSION}", first.stdout)
106
106
  self.assertIn("Downloading Neuron", first.stderr)
107
107
  asset = fake_release.current_asset()
108
- self.assertEqual(self.server.paths(), [f"/v{VERSION}/{asset}.sha256", f"/v{VERSION}/{asset}"])
108
+ self.assertEqual(self.server.paths(), [f"/v{VERSION}/SHA256SUMS", f"/v{VERSION}/{asset}.sha256", f"/v{VERSION}/{asset}"])
109
109
 
110
110
  offline = self.run_neuron(["prompt", "two words"], self.neuron_env(fake_release.unused_loopback_url(), NEURON_DUMMY_EXIT="7"))
111
111
  self.assertEqual(offline.returncode, 7, offline.stdout + offline.stderr)
@@ -189,11 +189,16 @@ class PackagedLauncherTests(unittest.TestCase):
189
189
  launcher, "PACKAGE_VERSION", version
190
190
  ), patch.dict(
191
191
  os.environ,
192
- {"NEURON_BINARY_PATH": "", "NEURON_CACHE_DIR": str(self.cache), "NEURON_DOWNLOAD_BASE": server.base_url},
192
+ {
193
+ "NEURON_BINARY_PATH": "",
194
+ "NEURON_CACHE_DIR": str(self.cache),
195
+ "NEURON_DOWNLOAD_BASE": server.base_url,
196
+ "NEURON_CHECKSUMS_BASE": server.base_url,
197
+ },
193
198
  ):
194
199
  binary = launcher._find_binary()
195
200
  requested = server.paths()
196
- self.assertEqual(requested, [f"/v{version}/{asset}.sha256", f"/v{version}/{asset}"])
201
+ self.assertEqual(requested, [f"/v{version}/SHA256SUMS", f"/v{version}/{asset}.sha256", f"/v{version}/{asset}"])
197
202
  self.assertEqual(binary, self.cache / version / asset)
198
203
  self.assertEqual(binary.read_bytes(), b"prerelease neuron")
199
204
 
@@ -241,8 +246,9 @@ class LauncherReleaseServerTests(unittest.TestCase):
241
246
  PYTHONPATH=str(self.site),
242
247
  NEURON_CACHE_DIR=str(self.cache),
243
248
  NEURON_DOWNLOAD_BASE=base,
244
- **extra_env,
245
249
  )
250
+ environment.setdefault("NEURON_CHECKSUMS_BASE", base)
251
+ environment.update(extra_env)
246
252
  return environment
247
253
 
248
254
  def run_launcher(self, base: str, *args: str, **extra_env: str) -> subprocess.CompletedProcess:
@@ -268,8 +274,8 @@ class LauncherReleaseServerTests(unittest.TestCase):
268
274
  self.assertIn(f"neuron-dummy {self.version}", result.stdout)
269
275
  self.assertIn("Downloading Neuron", result.stderr)
270
276
  prefix = f"/v{self.version}/{self.asset}"
271
- self.assertEqual(requested, [f"{prefix}.sha256", prefix])
272
- self.assertEqual(self.cached_files(), [self.asset, f"{self.asset}.sha256"])
277
+ self.assertEqual(requested, [f"/v{self.version}/SHA256SUMS", f"{prefix}.sha256", prefix])
278
+ self.assertEqual(self.cached_files(), [self.asset, f"{self.asset}.sha256", f"{self.asset}.verified"])
273
279
  digest = hashlib.sha256(staged.read_bytes()).hexdigest()
274
280
  sidecar = (self.cache / self.version / f"{self.asset}.sha256").read_text(encoding="ascii")
275
281
  self.assertEqual(sidecar, f"{digest} {self.asset}\n")
@@ -321,9 +327,96 @@ class LauncherReleaseServerTests(unittest.TestCase):
321
327
  fake_release.write_executable(stale, b"partial download")
322
328
  with fake_release.FakeReleaseServer(self.releases) as server:
323
329
  result = self.run_launcher(server.base_url, "--version")
324
- self.assertEqual(len(server.paths()), 2)
330
+ self.assertEqual(len(server.paths()), 3)
325
331
  self.assertEqual(result.returncode, 0, result.stderr)
326
- self.assertEqual(self.cached_files(), [self.asset, f"{self.asset}.sha256"])
332
+ self.assertEqual(self.cached_files(), [self.asset, f"{self.asset}.sha256", f"{self.asset}.verified"])
333
+
334
+ def test_release_sha256sums_must_list_the_asset_with_the_same_hash(self) -> None:
335
+ data = fake_release.dummy_binary_bytes(self.version)
336
+ digest = hashlib.sha256(data).hexdigest()
337
+ cases = {
338
+ "an empty SHA256SUMS": "",
339
+ "a SHA256SUMS without the asset": f"{digest} something-else\n",
340
+ "a SHA256SUMS with another hash": f"{'1' * 64} {self.asset}\n",
341
+ }
342
+ for name, sums in cases.items():
343
+ with self.subTest(name):
344
+ shutil.rmtree(self.releases, ignore_errors=True)
345
+ shutil.rmtree(self.cache, ignore_errors=True)
346
+ fake_release.stage_release(self.releases, self.version, self.asset, sums=sums)
347
+ with fake_release.FakeReleaseServer(self.releases) as server:
348
+ result = self.run_launcher(server.base_url, "--version")
349
+ self.assertEqual(result.returncode, 1, result.stdout)
350
+ self.assertIn("SHA256SUMS", result.stderr)
351
+ self.assertNotIn("Traceback", result.stderr)
352
+ self.assertEqual(self.cached_files(), [])
353
+ # No SHA256SUMS at all (HTTP 404).
354
+ shutil.rmtree(self.releases, ignore_errors=True)
355
+ fake_release.stage_release(self.releases, self.version, self.asset)
356
+ (self.releases / f"v{self.version}" / "SHA256SUMS").unlink()
357
+ with fake_release.FakeReleaseServer(self.releases) as server:
358
+ absent = self.run_launcher(server.base_url, "--version")
359
+ self.assertEqual(absent.returncode, 1)
360
+ self.assertIn("SHA256SUMS", absent.stderr)
361
+ self.assertEqual(self.cached_files(), [])
362
+ # A checksums server that is down fails closed as well, and must be HTTPS or loopback.
363
+ fake_release.stage_release(self.releases, self.version, self.asset)
364
+ with fake_release.FakeReleaseServer(self.releases) as server:
365
+ down = self.run_launcher(server.base_url, "--version", NEURON_CHECKSUMS_BASE=fake_release.unused_loopback_url())
366
+ insecure = self.run_launcher(server.base_url, "--version", NEURON_CHECKSUMS_BASE="http://downloads.example.com")
367
+ self.assertEqual(down.returncode, 1)
368
+ self.assertIn("SHA256SUMS", down.stderr)
369
+ self.assertEqual(insecure.returncode, 1)
370
+ self.assertIn("NEURON_CHECKSUMS_BASE must be an HTTPS URL", insecure.stderr)
371
+ self.assertEqual(self.cached_files(), [])
372
+
373
+ def test_sha256sums_comes_from_github_by_default_even_with_a_mirror(self) -> None:
374
+ launcher = load_launcher(self.site)
375
+ with patch.dict(os.environ, {"NEURON_DOWNLOAD_BASE": "https://mirror.example.com/neuron"}):
376
+ os.environ.pop("NEURON_CHECKSUMS_BASE", None)
377
+ self.assertEqual(launcher._checksums_base(), launcher.DEFAULT_DOWNLOAD_BASE)
378
+ with patch.dict(os.environ, {"NEURON_CHECKSUMS_BASE": "http://127.0.0.1:8/"}):
379
+ self.assertEqual(launcher._checksums_base(), "http://127.0.0.1:8")
380
+ with patch.dict(os.environ, {"NEURON_CHECKSUMS_BASE": "http://downloads.example.com"}):
381
+ with self.assertRaises(launcher.LauncherError):
382
+ launcher._checksums_base()
383
+ self.assertEqual(launcher._parse_sums(f"{'AB' * 32} neuron-linux-x64\n{'c' * 64} *other\n", "neuron-linux-x64"), "ab" * 32)
384
+ self.assertEqual(launcher._parse_sums(f"{'c' * 64} *other\n", "other"), "c" * 64)
385
+ self.assertIsNone(launcher._parse_sums(f"{'c' * 64} neuron-linux-x64-extra\n", "neuron-linux-x64"))
386
+ self.assertIsNone(launcher._parse_sums("", "neuron-linux-x64"))
387
+
388
+ def test_a_cached_binary_that_changed_after_it_was_verified_is_refused(self) -> None:
389
+ fake_release.stage_release(self.releases, self.version, self.asset)
390
+ with fake_release.FakeReleaseServer(self.releases) as server:
391
+ first = self.run_launcher(server.base_url, "--version")
392
+ self.assertEqual(first.returncode, 0, first.stderr)
393
+ offline_url = fake_release.unused_loopback_url()
394
+ binary = self.cache / self.version / self.asset
395
+ self.assertTrue(binary.with_name(binary.name + ".verified").is_file())
396
+ warm = self.run_launcher(offline_url, "--version")
397
+ self.assertEqual(warm.returncode, 0, warm.stderr)
398
+
399
+ # Same size, different bytes, new mtime.
400
+ original = binary.read_bytes()
401
+ tampered = bytearray(original)
402
+ tampered[-2] ^= 0x01
403
+ binary.write_bytes(bytes(tampered))
404
+ later = binary.stat().st_mtime + 5
405
+ os.utime(binary, (later, later))
406
+ refused = self.run_launcher(offline_url, "--version")
407
+ self.assertEqual(refused.returncode, 1)
408
+ self.assertIn("does not match its recorded SHA-256", refused.stderr)
409
+ self.assertNotIn("neuron-dummy", refused.stdout)
410
+ self.assertNotIn("Traceback", refused.stderr)
411
+
412
+ # A forged stamp does not excuse a changed file.
413
+ binary.with_name(binary.name + ".verified").write_text("garbage\n", encoding="ascii")
414
+ self.assertEqual(self.run_launcher(offline_url, "--version").returncode, 1)
415
+
416
+ # Restoring the verified bytes makes it run again.
417
+ binary.write_bytes(original)
418
+ restored = self.run_launcher(offline_url, "--version")
419
+ self.assertEqual(restored.returncode, 0, restored.stderr)
327
420
 
328
421
  def test_arguments_and_exit_code_are_forwarded(self) -> None:
329
422
  fake_release.stage_release(self.releases, self.version, self.asset)
File without changes
File without changes