Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.d/1584.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- [#1584](https://github.com/Digital-Process-Tools/claude-supertool/issues/1584) The suite-wide network block stated its blind spot as one thing — "it binds `socket` in the pytest process only" — and six in-process egress routes stayed open behind that sentence. Measured from inside an armed context, `connect_ex`, `sendto`, `gethostbyname`, `gethostbyname_ex`, `gethostbyaddr` and `getnameinfo` all walked out, and `gethostbyname` returned a live-resolved address. All six are refused now, along with `sendmsg`. Nothing in the suite took them, so no green was ever wrong — the boundary was.
- The fix is not a longer list, because a longer list is read exactly as trustingly as a short one. `tests/_netblock.py` now carries a register classifying **every callable the `socket` module and the `socket.socket` type expose** as patched, reached-via-a-patched-route, local, inert, or open-with-a-reason, and `tests/test_netblock_egress_register_1584.py` goes red when a name arrives that nobody has classified. The four routes no in-process patch can reach — a child process, a C extension calling libc directly, a descriptor connected before the guard was armed, and `os.read`/`os.write` on such a descriptor — are named rather than counted as blocked.
- A register derived from the running interpreter can only see the platform it runs on, and #1584's could not see `socket.socket.ioctl` — Windows-only, classified nowhere, red on all four Windows legs of the next PR while macOS and Linux stayed green. It is `OPEN` now with its reason: it addresses no peer, but `SIO_RCVALL` puts the interface into promiscuous receive, and the traffic that then arrives at `recv` has no destination for a destination-refusing guard to check. `_netblock.PLATFORM_ONLY` states the Windows-only names so the register stops learning one of them per release, and a test asserts each is absent on POSIX and present on Windows — the presence half is observed by the Windows legs, not by whoever wrote the list.
1 change: 1 addition & 0 deletions changelog.d/1598.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- [#1598](https://github.com/Digital-Process-Tools/claude-supertool/issues/1598) The suite's `$1`-shim gate scanned `tests/test_*.py`, so a shim in a helper module returned the same clean zero. It now reads every Python module under `tests/`, recursively — `_gitshim.py`, `_git_decline.py`, `conftest.py` and everything in `tests/fixtures/` were never opened, and a *shared* shim is exactly what a helper module holds. The widened population produces one hit, `tests/_gitshim.py`'s `while`/`shift` dispatcher, which is the correct implementation of the pattern; it is exempted by its **shape** rather than by its filename, because `$1` inside a loop that consumes arguments is the argument under the cursor and has no position to slide to. Naming the file would have re-created the allowlist [#1412](https://github.com/Digital-Process-Tools/claude-supertool/issues/1412) deleted, and would bless every shim that file ever acquires.
10 changes: 8 additions & 2 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -504,9 +504,15 @@ The same trap one layer up: an op with both a `gql` and a `gql_safe` needs both

**`block_outbound` is already armed for every test — you do not opt in ([#1341](https://github.com/Digital-Process-Tools/claude-supertool/issues/1341)).** `tests/conftest.py` applies it in an autouse fixture across the whole suite, so a missed or misaimed stub fails at the socket instead of contacting a third party and passing on the reply. Until #1341 it was a per-file fixture you had to remember, which is the same shape as a linter nobody runs: the three leaks #1312 found were all in files that had not added it.

`tests/_netblock.py` refuses any non-loopback `connect` or `getaddrinfo` and names the host and what to stub. Loopback and `AF_UNIX` stay open, so `test_http_bounds.py` and the `claude-channel` suites are unaffected — those bind their own servers and the process under test is the one that answered.
`tests/_netblock.py` refuses any non-loopback `connect`, `connect_ex`, `sendto`, `sendmsg`, `getaddrinfo`, `getnameinfo`, `gethostbyname`, `gethostbyname_ex` or `gethostbyaddr`, and names the host and what to stub. Loopback and `AF_UNIX` stay open, so `test_http_bounds.py` and the `claude-channel` suites are unaffected — those bind their own servers and the process under test is the one that answered.

**What it does not cover, so you do not read its green as more than it is:** it binds `socket` in the pytest process only. A test that shells out to `supertool.py` or a preset gets an unpatched child, and nothing there is blocked. That is the same blind spot the static grep has for a *missing* stub — #1312 measured a grep for transport tokens at 2 of 3 live leaks against the socket recorder's 3 of 3, because the third leak contained no transport token at all.
**What it does not cover is a register, not a sentence ([#1584](https://github.com/Digital-Process-Tools/claude-supertool/issues/1584)).** This paragraph used to say "it binds `socket` in the pytest process only" and stop, which reads as a closed list. It was not one: `connect_ex`, `gethostbyname`, `gethostbyname_ex`, `gethostbyaddr`, `getnameinfo` and a UDP `sendto` all left an armed context, and `gethostbyname` came back with a live-resolved address. Nothing in the suite took them — the #1312 class is `urllib`, which routes through `getaddrinfo` and `connect` — so no green was ever wrong; the boundary was.

`_netblock.ROUTES` and `_netblock.SOCKET_ROUTES` now classify **every callable the `socket` module and the `socket.socket` type expose** as `PATCHED`, `VIA`, `LOCAL`, `INERT` or `OPEN`, and `tests/test_netblock_egress_register_1584.py` fails when a name arrives that nobody has classified. Read the register rather than this paragraph: the register is derived from the running interpreter and this paragraph is not.

Derived from the *running* interpreter, which is where that check ends and `_netblock.PLATFORM_ONLY` starts. `socket.socket.ioctl` exists only on Windows, so no POSIX author and no POSIX leg could see it: it was classified nowhere and went red on all four Windows legs one PR after the register shipped. The names CPython guards with `MS_WINDOWS` are listed there and classified above, and a test asserts each is present exactly on its own platform — an absence on macOS and Linux, a presence on the Windows legs, which is what turns a claim read out of CPython's source into an observation. `ioctl` itself is `OPEN`, not `PATCHED`: it addresses no peer, but `SIO_RCVALL` puts the interface into promiscuous receive, after which a plain `recv` returns traffic between other hosts that this guard cannot tell from loopback.

`_netblock.BEYOND_THE_PROCESS` names the four routes no in-process patch can reach — a child process (a test that shells out to `supertool.py` or a preset gets an unpatched interpreter), a C extension calling libc directly, a descriptor connected before the guard was armed, and `os.read`/`os.write` on such a descriptor. That first one is the same blind spot the static grep has for a *missing* stub — #1312 measured a grep for transport tokens at 2 of 3 live leaks against the socket recorder's 3 of 3, because the third leak contained no transport token at all.

Why this is a rule and not a nicety: a live call makes the leg a statement about somebody else's DNS and redirect policy. #1312 was filed off a red on PR #1302, whose diff was one test file and one markdown table; Hashnode had started answering 301, `_http.urlopen` correctly refused the off-origin hop, and the test rendered that correct refusal as a product defect.

Expand Down
Loading