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.
- {neuroncli-7.0.0a4 → neuroncli-7.0.0a5}/NOTICE +11 -6
- {neuroncli-7.0.0a4/neuroncli.egg-info → neuroncli-7.0.0a5}/PKG-INFO +16 -14
- {neuroncli-7.0.0a4 → neuroncli-7.0.0a5}/README.md +15 -13
- {neuroncli-7.0.0a4 → neuroncli-7.0.0a5}/neuron_cli/__init__.py +88 -7
- {neuroncli-7.0.0a4 → neuroncli-7.0.0a5/neuroncli.egg-info}/PKG-INFO +16 -14
- {neuroncli-7.0.0a4 → neuroncli-7.0.0a5}/pyproject.toml +1 -1
- {neuroncli-7.0.0a4 → neuroncli-7.0.0a5}/tests/test_install_scripts.py +65 -8
- {neuroncli-7.0.0a4 → neuroncli-7.0.0a5}/tests/test_npm_package.py +2 -2
- {neuroncli-7.0.0a4 → neuroncli-7.0.0a5}/tests/test_packaged_launcher.py +100 -7
- {neuroncli-7.0.0a4 → neuroncli-7.0.0a5}/LICENSE +0 -0
- {neuroncli-7.0.0a4 → neuroncli-7.0.0a5}/neuroncli.egg-info/SOURCES.txt +0 -0
- {neuroncli-7.0.0a4 → neuroncli-7.0.0a5}/neuroncli.egg-info/dependency_links.txt +0 -0
- {neuroncli-7.0.0a4 → neuroncli-7.0.0a5}/neuroncli.egg-info/entry_points.txt +0 -0
- {neuroncli-7.0.0a4 → neuroncli-7.0.0a5}/neuroncli.egg-info/top_level.txt +0 -0
- {neuroncli-7.0.0a4 → neuroncli-7.0.0a5}/setup.cfg +0 -0
- {neuroncli-7.0.0a4 → neuroncli-7.0.0a5}/tests/test_packaging_metadata.py +0 -0
- {neuroncli-7.0.0a4 → neuroncli-7.0.0a5}/tests/test_porting_workspace.py +0 -0
- {neuroncli-7.0.0a4 → neuroncli-7.0.0a5}/tests/test_release_workflow.py +0 -0
|
@@ -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
|
-
|
|
84
|
-
|
|
85
|
+
Default MCP servers
|
|
86
|
+
-------------------
|
|
85
87
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
npm
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
95
|
-
`pip index versions neuroncli --pre` should list `7.0.
|
|
96
|
-
previous alpha (`7.0.0-alpha.
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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.
|
|
120
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
69
|
-
`pip index versions neuroncli --pre` should list `7.0.
|
|
70
|
-
previous alpha (`7.0.0-alpha.
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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.
|
|
94
|
-
|
|
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
|
|
5
|
-
with a ``.sha256`` sidecar
|
|
6
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
95
|
-
`pip index versions neuroncli --pre` should list `7.0.
|
|
96
|
-
previous alpha (`7.0.0-alpha.
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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.
|
|
120
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
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(),
|
|
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
|
-
{
|
|
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()),
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|