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
78 changes: 78 additions & 0 deletions .github/claude-review-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# What a review of this repository has to know

This file is the reviewer's briefing. It is copied to `CLAUDE.md` on the CI runner before
the review runs, because the maintainer's own `CLAUDE.md` is not in the repository - it
lives in a private notes repo and a runner never sees it. Without this file the review
arrives with no idea what this project holds itself to and spends its findings on textbook
advice that is already handled.

Everything below is already visible in `CONTRIBUTING.md` and the READMEs. Nothing private
belongs here: this file is public and permanent, like every other file in a public repo.

## What the tool is

Bean Network Tester simulates poor network conditions on Windows - latency, packet loss,
jitter, bandwidth limits, dropped connections - so a developer can see how their program
behaves on a bad link. It has a GUI and a CLI, and it works by loading the WinDivert kernel
driver and mangling packets in flight.

Two consequences worth carrying into every review:

- **It runs on the machine it is testing.** A change that widens what gets intercepted can
take the user's own network down with it. Impairment must always be narrow - a target
process, address or port - and bounded by a duration.
- **It ships a kernel driver to strangers.** Supply chain, signatures and pinned bytes are
not paperwork here; they are the product.

## Rules that a reviewer should treat as blocking

1. **Flat hyphen only.** No em dash, no en dash, anywhere in the repository - code,
comments, docs, changelogs. A test enforces it.
2. **Everything in git is English.** Commit messages, pull request titles and bodies. Quote
the program - interface text, command output, code - and never a person: a sentence from
a conversation is somebody else's words, and a public commit cannot be unpublished. No
local paths, machine names, addresses or tokens, in comments either.
3. **Anything visible from outside goes in the changelog.** `CHANGELOG.md` for users,
`CHANGELOG-INTERNAL.md` for maintainers; a GUI change counts as visible. Entries go under
`[Unreleased]`, and `VERSION.txt` is never bumped in a pull request.
4. **Never break traffic globally.** A real interception needs a narrow target
(`--target` / `--dst-ip` / `--dst-port`) and a short `--duration`. `--loss` or `--latency`
with no target is a defect, not a default.
5. **Fail open.** Anything that could leave the WinDivert handle open must stop the engine
instead. Traffic is released on failure, never held.
6. **New behaviour arrives with the test that guards it.** A new failure mode gets an exit
code, a test and a README row. A new mechanism in the decision pipeline gets unit tests.
A test that cannot fail is worse than no test.

## Contracts that changes must not break silently

- **The CLI is a CI/CD interface.** Every outcome has an exit code from
`beantester/exitcodes.py`. Logs go to stderr, data goes to stdout, as text or NDJSON.
Changing a code, a stream or the NDJSON schema is a breaking change.
- **UI text lives in `lang/<code>.json`, never in code.** Code carries i18n keys. A new key
goes into `lang/en.json` **and** `lang/pl.json` in the same change, or the other language
falls back silently.
- **`BeanCore.decide()` stays pure.** It is the decision pipeline and it is covered
position by position.
- **Presets are ordered best at the top, worst at the bottom.**
- **The project website's page addresses are a contract.** The site is published; names on
its pages come from the language files, not typed by hand.

## What CI already enforces, so a review need not

ruff (bug shapes, dead code, a measured complexity ceiling), mypy, semgrep, CodeQL, a
coverage gate on the whole repository plus 80 percent on the lines a pull request changes,
a mutation registry that re-breaks each guarded behaviour to prove its test reddens, a
licence gate on new dependencies, a weekly dependency audit, and a check that commit
messages and the pull request body obey rule 2. Every action is pinned to a commit SHA and
no `${{ }}` is ever interpolated into a `run:` script.

Findings that repeat one of those are noise. The valuable finding is the one no gate can
see: a wrong answer, a broken edge case, a contract quietly changed, a test that passes for
the wrong reason, a comment that no longer matches the code beneath it.

## How to write a finding here

Say what breaks, with the input or state that breaks it. "This could be clearer" is not a
finding; "with `--duration 0` this loops forever, and no test covers it" is. If a rule above
is broken, name the rule. If nothing is wrong, say so plainly rather than filling the space.
111 changes: 111 additions & 0 deletions .github/workflows/claude-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# A second reader on every pull request, with a different context from the session
# that wrote the code.
#
# Why it is worth a workflow when four analysers already run: they read the code for
# shapes. This reads the CHANGE for meaning - a wrong answer, an edge case nobody
# tried, a contract quietly altered, a comment that stopped matching the code under
# it. None of those redden a linter.
#
# 🔴 It cannot fail a pull request. It comments.
#
# 🔴 **A pull request that CHANGES THIS FILE gets no review, and the job still goes
# green.** Measured on the pull request that introduced it (2026-08-19): the app
# refuses to hand out a token unless the workflow file is byte-identical to the copy
# on the default branch, and the action then exits with
# *"Exiting due to workflow validation skip"* - a success, in 18 seconds, with no
# model call and nothing spent. That is the app's anti-abuse rule, and it is the
# right one: without it a pull request could edit this file to walk off with the
# token. The consequence to remember rather than re-derive: this workflow cannot be
# tested before it is merged, and every later change to it skips its own review.
name: Claude review

on:
# `opened` and `ready_for_review` only, deliberately - NOT `synchronize`.
# `synchronize` fires on every push, and at this project's rate (several pull
# requests a day, several pushes each) that multiplies the bill by the number of
# times somebody amends a branch. The cost of the choice is named rather than
# hidden: a review reads the pull request as opened, so a finding introduced by a
# later push is not seen. Ask for a fresh pass with an `@claude` comment when a
# branch changes substantially.
pull_request:
types: [opened, ready_for_review]

# Read-all at the top; the job raises what it needs. Same rule as every other
# workflow here.
permissions:
contents: read

# One review per pull request. Reopening or marking ready while a review is still
# running replaces it rather than paying for both.
concurrency:
group: claude-review-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
review:
name: Claude review
# 🔴 THE COST GATE, and it is three locks rather than one, because this is the
# only job here that spends money per run.
#
# 1. this condition - the pull request must be the maintainer's;
# 2. the action's own check - it refuses an actor without write access, and
# refuses bot actors outright, so Dependabot never triggers it;
# 3. GitHub itself - a public repository withholds secrets from workflows
# triggered by a fork's pull request, so a stranger's branch cannot spend
# the token even if the two above were removed.
#
# Drafts are skipped: `ready_for_review` is in the trigger precisely so the
# review happens once, when the change is finished.
if: >-
github.event.pull_request.user.login == 'donislawdev'
&& github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
pull-requests: read
issues: read
# Required by the action's default GitHub App authentication.
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1

# 🔴 The reviewer's briefing, and the reason it needs a step at all.
#
# Claude Code reads `CLAUDE.md` from the checkout as project memory - that is
# the documented way to give it a project's rules. This repository's real
# `CLAUDE.md` is git-ignored: it lives in a private notes repo, so a runner
# checks out a tree without it and the review would arrive knowing nothing
# about flat hyphens, English-only history, the changelog rule or the fail-open
# contract, and would spend its findings on advice CI already enforces.
#
# So the public digest in `.github/` is copied into place for the length of the
# run. Nothing private crosses over: that file is in the repository, and every
# rule in it is already stated in CONTRIBUTING.md and the READMEs.
- name: Put the public rule digest where Claude reads project memory
shell: bash
run: cp .github/claude-review-rules.md CLAUDE.md

- name: Review the pull request
uses: anthropics/claude-code-action@d40ddef4c030e508327d6e35a9c45f3368482c50 # v1.0.195
with:
# The subscription token, not an API key: runs bill against the
# maintainer's Claude subscription instead of opening a second meter.
# Generated with `claude setup-token`.
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: "https://github.com/anthropics/claude-code.git"
plugins: "code-review@claude-code-plugins"
# `--comment` is what puts the review on the pull request - an inline
# comment per finding, or one summary comment when there are none. Without
# it the findings stay in the run log, where nobody reads them.
prompt: "/code-review:code-review --comment ${{ github.repository }}/pull/${{ github.event.pull_request.number }}"
# `--model` because the default is whatever Claude Code ships; this project
# would rather pay for the better reader on a change that ships a kernel
# driver. `--allowedTools` has to name the inline-comment tool even though
# the skill's own frontmatter does: the action starts that MCP server only
# when this argument asks for it.
claude_args: |
--model claude-opus-5
--allowedTools "mcp__github_inline_comment__create_inline_comment"
35 changes: 34 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,42 @@ jobs:
# built BY the action from the workflow's own context - there is nothing to
# pass in, and nothing we could pass in that would be trustworthy.
- name: Attest the build provenance of the release archive
id: provenance
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: ${{ env.ASSET }}

# The same bundle, published as an ASSET rather than left only in the
# attestation store. Two reasons, one for a person and one for a scanner.
#
# For a person: the file travels with the archive it describes, so
# `gh attestation verify <zip> --bundle <this file>` answers without asking an
# API - which is the difference between "GitHub says this is fine" and "these
# bytes say so". A mirror that copies the release page copies the proof too.
#
# 🔴 For a scanner: OpenSSF Scorecard's Signed-Releases check reads release
# assets BY FILE EXTENSION (`.sigstore.json`, `.asc`, `.sig`, `.intoto.jsonl`)
# and never looks in the attestation store. Read in probes/releasesAreSigned
# on 2026-08-19, after the check scored 0/10 on releases that already carried
# two attestations. Producing evidence nobody can find is the same as not
# producing it.
#
# The name says what the file IS: a Sigstore bundle, which is what the action
# writes. `.intoto.jsonl` would score two points higher there and would be a
# different format - not a rename.
- name: Publish the provenance bundle beside the archive
shell: bash
env:
# Through the environment, never interpolated into the script: `${{ }}` is
# expanded before a shell exists, so it is source code rather than an
# argument. Guarded by tests/test_repo_conventions.py.
BUNDLE_PATH: ${{ steps.provenance.outputs.bundle-path }}
run: |
bundle="BeanNetworkTester-${GITHUB_REF_NAME}.sigstore.json"
cp "$BUNDLE_PATH" "$bundle"
python -c "import json,sys; json.load(open(sys.argv[1])); print('bundle parses as JSON')" "$bundle"
echo "BUNDLE=$bundle" >> "$GITHUB_ENV"

# gh is preinstalled on the runner - no third-party action, uses the job token.
# A -rc/-beta/-alpha tag publishes as a "Pre-release"; a plain tag as "Latest".
#
Expand All @@ -212,4 +244,5 @@ jobs:
head -5 RELEASE_NOTES.md
flags=(--title "$TITLE" --notes-file RELEASE_NOTES.md)
if [ "$PRERELEASE" = "true" ]; then flags+=(--prerelease); else flags+=(--latest); fi
gh release create "$GITHUB_REF_NAME" "$ASSET" SHA256SUMS.txt "$SBOM" "${flags[@]}"
gh release create "$GITHUB_REF_NAME" \
"$ASSET" SHA256SUMS.txt "$SBOM" "$BUNDLE" "${flags[@]}"
15 changes: 11 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/); versions fol

- **You can now check where a download came from, not just that it is unchanged.** Every
release archive carries a signed build attestation, so one command answers "was this really
built from that source, by that workflow":
built from that source":
`gh attestation verify BeanNetworkTester-v0.5.0-windows-x64.zip -R donislawdev/BeanNetworkTester`.
A checksum proves the file matches the release page. This proves the release page itself came
out of this repository's own workflow, from a specific commit. The same command also verifies
the SBOM shipped beside the archive.
A checksum proves the file matches the release page; this proves the page came out of this
repository's workflow, from a specific commit. It covers the SBOM too. The proof now ships
**as a file** as well, `BeanNetworkTester-vX.Y.Z.sigstore.json`, so `--bundle` checks the
archive from a mirror, or with no network at all.

### Changed

Expand All @@ -39,6 +40,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/); versions fol

### Fixed

- **A filter ending in a backslash no longer swallows the one after it.** Type a Windows path with
its trailing separator and a second name - `C:\, chrome.exe` - and the tool showed two filters
while using one, because that backslash ate the comma between them. The stray backslash is now
dropped as the filter is read. It never meant anything on its own, so nothing you can usefully
write is affected, and what the filter line says is what the filter does.

- **Column tooltips in Connections no longer describe the wrong column.** With any column hidden,
every header to its right explained its neighbour instead - and with only a couple of columns
left, the tooltip could describe a column that was not on screen at all. The tooltip now follows
Expand Down
5 changes: 4 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ onedir, `asInvoker`. Do not reintroduce `--noconsole` / `--onefile` / `--uac-adm
- Presets are ordered best (top) -> worst (bottom).
- Keep `BeanCore.decide()` pure and covered by tests; new mechanisms get a
numbered spot in the pipeline plus unit tests.
- A new failure mode gets an exit code, a test and a README row. A new mechanism gets a numbered spot in the pipeline plus unit tests
- **New functionality is merged with the test that guards it.** That is the policy, and the
two bullets above are what it looks like in practice. It is not left to good intentions:
`tests/test_mutation_registry.py` records which broken behaviour each test is supposed to
catch, and CI re-breaks them to prove the test actually reddens.

## Pull requests

Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
[![Downloads](https://img.shields.io/github/downloads/donislawdev/BeanNetworkTester/total)](https://github.com/donislawdev/BeanNetworkTester/releases)
[![License: GPLv3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/donislawdev/BeanNetworkTester/badge)](https://scorecard.dev/viewer/?uri=github.com/donislawdev/BeanNetworkTester)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/14154/badge)](https://www.bestpractices.dev/projects/14154)
![Platform: Windows](https://img.shields.io/badge/platform-Windows-0078D6)

**Bean Network Tester** is a tool for testers and developers: check how your application behaves
Expand Down Expand Up @@ -1485,6 +1486,16 @@ A checksum proves the file matches what the release page says. This proves the r
was produced by this repository's own workflow, from a specific commit, on a GitHub-hosted runner.
The same command also verifies the SBOM that ships beside the archive.

That command asks GitHub. The proof also ships **as a file**, `BeanNetworkTester-vX.Y.Z.sigstore.json`,
so you can check the archive without one:

```bash
gh attestation verify BeanNetworkTester-v0.5.0-windows-x64.zip --bundle BeanNetworkTester-v0.5.0.sigstore.json
```

Useful if you got the files from a mirror, or from a machine that cannot reach the API - the
evidence travelled with the download instead of living somewhere you have to trust separately.

### What is inside the download, and how to check it

Every release carries an **SBOM** - a list, in the standard SPDX format, of every third-party
Expand Down
11 changes: 11 additions & 0 deletions README.pl.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
[![Downloads](https://img.shields.io/github/downloads/donislawdev/BeanNetworkTester/total)](https://github.com/donislawdev/BeanNetworkTester/releases)
[![License: GPLv3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/donislawdev/BeanNetworkTester/badge)](https://scorecard.dev/viewer/?uri=github.com/donislawdev/BeanNetworkTester)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/14154/badge)](https://www.bestpractices.dev/projects/14154)
![Platform: Windows](https://img.shields.io/badge/platform-Windows-0078D6)

**Bean Network Tester** to narzędzie dla testerów i deweloperów: sprawdź, jak aplikacja zachowuje
Expand Down Expand Up @@ -1347,6 +1348,16 @@ Suma kontrolna dowodzi, że plik zgadza się z tym, co mówi strona wydania. To
strona wydania powstała z workflow tego repozytorium, z konkretnego commita, na maszynie GitHuba.
Tym samym poleceniem sprawdzisz też SBOM, który jedzie obok archiwum.

To polecenie pyta GitHuba. Dowód jedzie też **jako plik**, `BeanNetworkTester-vX.Y.Z.sigstore.json`,
więc archiwum sprawdzisz bez pytania kogokolwiek:

```bash
gh attestation verify BeanNetworkTester-v0.5.0-windows-x64.zip --bundle BeanNetworkTester-v0.5.0.sigstore.json
```

Przydaje się, gdy pliki masz z kopii lustrzanej albo na maszynie bez dostępu do API - dowód
przyjechał razem z pobranym plikiem, zamiast leżeć w miejscu, któremu trzeba osobno ufać.

### Co jest w środku pobranego pliku i jak to sprawdzić

Każde wydanie niesie **SBOM** - listę, w standardowym formacie SPDX, wszystkich
Expand Down
Loading
Loading