Release 0.4.0 - #78
Merged
Merged
Conversation
Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](pytest-dev/pytest@8.0.0...9.1.1) --- updated-dependencies: - dependency-name: pytest dependency-version: 9.1.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [iroh](https://github.com/n0-computer/iroh) from 1.0.3 to 1.1.0. - [Release notes](https://github.com/n0-computer/iroh/releases) - [Changelog](https://github.com/n0-computer/iroh/blob/main/CHANGELOG.md) - [Commits](n0-computer/iroh@v1.0.3...v1.1.0) --- updated-dependencies: - dependency-name: iroh dependency-version: 1.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
The user set priorities on 2026-09-14: the receiver sees what is coming and accepts before anything is written, either side can cancel, the sender sees the receiver's state throughout, NAT traversal is proven rather than assumed, and drop works on Windows, macOS and Linux with transfers between them. Two new plans. Receiver consent and status supersedes the 2026-08-19 confirmation plan, which was written against cleartext metadata. Cross-platform records that Windows has no build and macOS has never had a test run. The browser removal and browser-on-iroh plans from 2026-09-11 are committed, with the removal's four open questions answered as the plan leaned. A re-run of netlab found that hole punching has never been exercised there: QUIC address discovery fails TLS against the lab helper's self-signed certificate, so no peer learns its public address. The helper's comment and the rendezvous plan both claimed iroh skips that check; it does not. Recorded in the netlab plan, with the fix proposed as self-hosted rendezvous phase 3 and left for a decision because it changes what the CLI trusts. Also corrected: meta_ok confirmation was to land before the direct path shipped, and the direct path shipped in v0.2.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
`install.sh` has nothing to do with the browser client. It lived in `web/public/` only because entry 8's split deployment served it as a static file. The release workflow publishes it from that path under `fail_on_unmatched_files`, so deleting `web/` first would fail the next tag in `publish`, after the whole build matrix had already succeeded. Moving it first keeps the release path intact through every commit of the removal. The relay's `/install.sh` route goes too. It served the file so a curl against the hosted relay worked, and entry 16 removed that host. The README already points at the release asset. The script is byte-identical to the v0.3.0 release asset, and `DROP_VERSION=v0.3.0` installs a binary that reports 0.3.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
No `cfg(not(unix))` branch in the CLI has ever been compiled: CI runs on Ubuntu alone, and the release workflow only ever executes `drop --version` on macOS and has no Windows target. Cross-platform phase 0 is to find out what breaks before fixing any of it, so this adds Clippy and the full test suite on `macos-14` and `windows-2025`. A separate job rather than a matrix on `rust`: formatting cannot vary by platform, and branch protection requires a check named exactly "Rust", which a matrix would rename. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
The receiver printed the sender's filename verbatim in its "Receiving" line, before the receiver had agreed to anything. A name carrying an escape sequence could clear the line, move the cursor, retitle the window or plant a hyperlink, and a right-to-left override could make `exe.pdf` read as `fdp.exe`. Error messages from a peer, or from the relay, which is equally untrusted, reached the terminal the same way. `display.rs` replaces control characters and bidirectional and invisible formatting characters with U+FFFD rather than deleting them, so a doctored name looks doctored. It collapses whitespace padding and shortens long names in the middle, keeping the extension. Honest names in any script, emoji included, pass through unchanged. Applied to every place where someone else's text is printed. Pinned end to end through the real binary over a real relay. With the fix reverted, the test fails on the escape sequence reaching the receiver. Receiver consent plan, phase 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
The first Windows run failed Clippy on two items only the symlink tests use: an import in the archive tests and the hostile-archive builder. Everything else, including every cfg(not(unix)) branch in the CLI, compiled. The test step now runs even when Clippy fails, so one run reports both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
Entry 16 removed the browser client's audience: with no hosted relay, only someone self-hosting the full-stack image could reach it. It had also never been exercised by a browser, and every wire change planned for 0.4.0 would have had to be carried through it untested. Gone: `web/`, `crypto-wasm/`, the relay's `/` and `/assets` routes, CORS and `DROP_ALLOWED_ORIGINS`, `Dockerfile.fullstack`, the `web` CI job, npm in dependabot, and `.cargo/config.toml`'s wasm rustflag. `GET /` now answers 404 with one line saying what the host is. CodeQL analyses Rust instead of the TypeScript that no longer exists. The relay stays. `--transport relay` is what works on a network that lets no UDP out, and netlab covers that. The rules about what a browser transfer may be called stay too, marked dormant, since they bind any future browser client. Found on the way: `Dockerfile` has not built since the envelope moved into its own crate. It copied the CLI's manifest but not `crypto/`, so cargo could not load the workspace. Reproduced by copying exactly the Dockerfile's files into a scratch directory, and fixed there with `--release --locked`. Recorded as decisions entry 17; entry 11 is marked superseded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
The relay refuses a frame that is too large from its header and closes the socket, which can happen while the test is still writing the frame's body. Linux's socket buffer usually absorbs that write; macOS's usually does not, and the second CI run on macOS failed with a broken pipe. The relay behaved correctly both times. The write may now fail by the relay hanging up. What is asserted is still what the receiver saw: an error, and no part of the chunk. Also records what the first macOS and Windows runs found in the cross-platform plan. Windows passed the whole suite once two Unix-only test helpers were gated, and every non-Unix branch in the CLI compiled for the first time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
Before this, a folder with a symlink in it stopped extracting on Windows at the first link. Names ordinary on Linux and macOS were worse. `notes.txt:hidden` would have become an NTFS stream on `notes.txt`, `CON` a device, `report.` a file quietly named `report`, and `a<b` an error that ended the extraction. Names are rewritten, not refused, because most of them come from honest senders: `10:30 standup.md` is a normal name on a Mac. Each character Windows would interpret becomes `_`, trailing dots and spaces become `_`, and a device name gets `_` after its stem. A rewrite never introduces a separator, so it cannot move an entry to another directory. The existence and link checks run on the rewritten path, and every rename is reported. Archive paths are now split on `/` by hand rather than handed to `Path`, which reads `C:x` as a drive on Windows and as a name elsewhere, so every platform judges the same components. A name the filesystem refuses, or a link the system cannot create, now skips that entry with a warning instead of ending the extraction. A full disk or a permission problem still ends it. Pinned without Windows by a 300-byte name; with the old behaviour restored, that test fails. Cross-platform plan, phase 1. The Windows-only tests run in CI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
It does not. The relay repeats the few envelope constants it enforces, and cli/tests/protocol.rs fails if a copy drifts. architecture.md said otherwise, and the removal carried that claim into the Dockerfile, two manifest comments, lib.rs and decisions entry 17. The Dockerfile change itself stands: cargo needs crypto/ because it is a workspace member and the CLI's path dependency. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
Entry 13 has the sender count guesses and ask a human before allowing another, so that being probed is visible. It rested on `meta_ok`, a bare claim made by the party being limited. A guesser who could not open the metadata could say `meta_ok` anyway, and the counter never climbed. Nothing readable ever leaked, but the noticing did not hold. `meta_ok` now carries a key confirmation, a fourth HKDF output only a peer holding the same keys can produce. The sender compares it in constant time, inside `crypto/` so a later `==` cannot creep in at a call site. A missing, malformed or wrong one is a failed attempt like any other. Both carriers run the checkpoint. The relay forwards `meta_ok` rather than refusing it, and over the relay the sender reads past the relay's own `sending` and `progress` narration, which would otherwise count as a failed guess. A failure there ends the transfer, since the relay has already burned the session. `ENVELOPE_VERSION` and `DROP_ALPN` both become 2, and a test ties them together. A 0.3.0 peer or relay is refused, and the relay's error now names both versions. The direct path already shipped in v0.2.0, so this is a wire break; the consent work lands under the same unreleased version. Test peers now run a real handshake through `ScriptedTransport::responding`, since no fixed script can prove it holds keys derived from the sender's fresh half. With the comparison forced true, the claims test fails on a bare meta_ok. Recorded as decisions entry 18. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
A receiver used to learn what it was getting as it arrived, and the file was created before any question could be asked. Now, once the key confirmation passes, the receiver is shown the name, a type label from the real extension (with a warning for programs), the size, a folder's file count and unpacked size, and exactly where it would be saved. Nothing is created until they accept. The destination is planned by looking rather than creating, so declining leaves the directory byte-identical, and the name shown is the name written. A question unanswered for two minutes is declined. Without a terminal, `drop recv` refuses to start unless given `--yes`, and refuses before contacting anything so the sender's code is not spent. The relay learns `accept`, `decline`, `cancel` from either side and `finishing`. It carries no chunk before `accept`, forwards reasons only from a fixed set, and counts declines and cancels apart from failures. The sender waits for the answer and says what the receiver is doing. While the question is open the receiver keeps reading, because a relay drops a socket that stops answering pings. That read is abandoned when the person answers, so receiving must be cancel-safe, and the QUIC framing was not: `read_exact` into a local buffer loses a partial frame when dropped. It now keeps the partial frame in the transport. Negative controls: without the relay's gate the early-chunk test fails; with a file created on the decline path the decline test fails; the old framed reader fails the cancel-safety test. Consent plan phases 2 and 3; decisions entry 19. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
The first Windows run of the naming tests showed that storing `C:x` as sent there is refused: PathBuf::push treats it as drive-relative and replaces the destination, and the final inside-the-destination check catches that. Names are never stored as sent on Windows, so the refusal is the correct outcome and the test now asserts it, pinning the safety check that found it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
Ctrl-C used to exit without a word. The peer found out from a dropped connection, and the relay counted it as a failure. Now the first Ctrl-C (or SIGTERM) cancels: whatever the transfer waits on next sends the peer `cancel` and stops, and the other side says who stopped it. A second Ctrl-C, or two seconds without stopping, quits at once as before. The mechanism is a wrapper around the transport, since every wait in both directions already goes through one. Sends are refused before they start, never interrupted, so no frame is left half-written. Two bugs found on the way, both covered by tests: - A receiver left a partial file behind on any early exit other than an integrity failure, including a dropped connection. A guard now deletes it unless every byte arrived and was verified. - A sender that was mid-write when the receiver cancelled reported "Broken pipe": the relay forwards `cancel` and closes, and the write fails first. A failed write now reads what already arrived and reports the peer's reason. Exit statuses: 3 declined or not answered, 4 cancelled by the other side, 130 cancelled here. With --status each side prints `drop-status: state=` lines, which netlab now asserts in the relayed topology. Consent plan phase 4; decisions entry 20. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
On Windows, where a PathBuf is larger, the two fields receiver consent added to Payload pushed Attempt::FailedTheCode past Clippy's large-variant limit, failing CI there only. The payload is boxed; nothing reads it except the retry that hands it back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
…ow closes Three things only a Windows sender got wrong: - Link targets were recorded as `read_link` gave them, `..\shared\config`, which a Linux or macOS receiver takes as one name with backslashes in it. Relative targets are now written with `/`, and absolute ones, which would dangle on any other machine, are left out with a reason. - Closing the console window, logging off and shutting down are not Ctrl-C on Windows, and nothing handled them, so a compressed send's spool file (a copy of the user's data) stayed in %TEMP%. They now start the same cancel-and-clean path as Ctrl-C, as does Ctrl-Break. - The progress line assumed escape sequences work. The older console that cmd.exe still opens prints them literally until VT processing is turned on. It is turned on now, and where that fails the line is redrawn with padding instead. Also pinned on every platform: a spool file can be deleted while the chunk reader has it open, which on Windows depends on how the file was opened. The Windows-only code is compiled first by the Windows CI runner. Cross-platform plan, phase 2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
The release workflow builds x86_64-pc-windows-msvc, and aarch64 on an Arm runner, as zips holding drop.exe. The C runtime is linked statically, so it starts on a machine without the Visual C++ redistributable. The Arm build is marked continue-on-error, since a toolchain problem there must not hold back a release. Checksums cover the zips, and install.ps1 is published beside install.sh. install.ps1 is the Windows counterpart of install.sh, with the same three variables. It verifies the checksum, installs to %LOCALAPPDATA%\Programs\drop, and adds that directory to the user's own PATH, without elevation. It runs in its own script block and fails with `throw`, because through `irm | iex` a bare `exit` would close the user's PowerShell window. install.sh run from Git Bash now points at it instead of saying the OS is unsupported. A new Installer CI job runs both installers for real on every pull request. It serves locally built packages laid out like a release, checks the installed binary runs, and checks that a tampered checksum is refused and installs nothing. install.ps1 runs under both pwsh 7 and Windows PowerShell 5. The Linux half was run locally. Cross-platform plan, phase 3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
Only a normal completion drained the sender's socket before dropping it. When a receiver declined or cancelled mid-stream, the relay stopped reading the sender's chunks, so its close became a TCP reset. Linux keeps the preceding `cancel` frame readable after a reset; Windows discards unread data, so the Windows CI run of the receiver-cancel test saw "connection aborted" instead of the receiver's reason. Both socket tasks now drain until the peer answers the Close, whenever their loop ended with the socket still open, bounded by the existing deadlines. The download socket had no drain at all, which the teardown plan had recorded as a latent problem of the same shape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
A cross-OS transfer can differ by platform in only two places: how the sender reads a tree, and how the receiver writes one. The wire is the same everywhere. Two CI runners cannot swap a transfer code, so the transfer is split in two. `produce`, on each OS, builds a tree using every kind of name and file that OS can hold and archives it exactly as `drop send` does. `consume`, on each OS, receives every OS's archive, tar and gzipped, through the real receive path over an in-process relay. It checks contents, directories, links (kept on Unix, skipped on Windows), executable bits, and Windows name rewriting. That is nine pairings. The workflow runs on pull requests that touch the tree code, nightly, and on demand. With one byte of an archive flipped, consume fails naming the file. Cross-platform plan, phase 4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
A direct-path sender ended its whole transfer if anything reached its socket first with a QUIC handshake that failed. The network lab traced the intermittent `authentication failed` to exactly that: a late packet from a relay's address-discovery port was taken for an incoming connection, failed, and ended the send before the real receiver dialled. Plain LAN passed 1 run in 5 on main. On a public address, anyone who can send one UDP packet could do the same on purpose. A failed handshake is now dropped and the wait goes on. It is not the receiver, which completes the handshake, and not a guess, which needs a completed handshake first, so the one-guess rule is untouched. After the fix, plain LAN passed 8 runs in 8. A unit test sends a junk QUIC Initial to a waiting sender and then connects a real receiver; with the old behaviour it fails 3 times in 3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
The Windows CI run showed the sender still reporting "connection aborted" when the receiver cancelled, after the relay learned to drain. The sender only read when its window was full, so it never answered the relay's close. The drain expired, the relay reset the connection, and Windows discarded the unread `cancel` along with everything else. Reading after the failed write, which works on Linux, cannot get it back there. The sender now reads whatever has already arrived before each chunk, without waiting, which is safe because a receive must be cancel-safe. Acknowledgements move the window as before, and a `cancel` ends the transfer with the receiver's reason while it is still readable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
One Windows run of the installer job started install.ps1 before the local Python server was listening, and the download failed. Both halves of the job now poll the server until it answers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7Fy45hUvna94cd79WKG8S
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0.2.0 and 0.3.0 went out with empty release pages: the notes lived in a commit message and nothing carried them to GitHub. A release now reads docs/releases/vX.Y.Z.md as its text, and the verify job refuses a tag without one before anything is built, because the release action only warns about a missing file and publishes an empty page anyway. AGENTS.md gets the rules for writing them: three headings at most (Before you upgrade, Added, Fixed), one short bullet per change a user would notice, and nothing about refactors, CI, dependencies or PR numbers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A minor bump, and a breaking one: protocol version 2 means 0.4.0 does not transfer with 0.3.0 or older, on either path or through an older relay (decisions.md entry 18). `drop recv` without a terminal now needs `--yes`. The notes are docs/releases/v0.4.0.md. Also here: - The README platform table no longer says Intel macOS and aarch64 Linux are tested in CI. CI tests on macos-14 and x86_64 runners; those two targets are only built and started by the release workflow. - Consent phase 6 ticked: its documents landed with phases 1-4. - The checklist item about a binary reporting the wrong version is gone; release.yml's verify job has refused a mismatched tag since 0.2.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Everything for 0.4.0 in one pull request: the twelve stacked PRs (#65 to #76), #77, and the two dependency updates (#63, #62). Each one was merged in order with a merge commit, so its history stays readable here and in its own PR.
The release notes are
docs/releases/v0.4.0.md. Please read them as part of this review, because they become the GitHub release text.On top of the merges
Cargo.tomlandCargo.lock.release.ymlrefuses a tag with nodocs/releases/<tag>.mdand publishes the file as the release text. (0.2.0 and 0.3.0 went out with empty pages.)AGENTS.mdnow has the rules for writing the notes.Merging
v0.4.0on the merge commit to start the release.Validation
cargo fmt --check, Clippy with-D warningsandcargo test --workspace --all-targetsall pass. 253 tests passed, 0 failed, and 2 ignored (the cross-OS tests, which CI runs).Not proven
🤖 Generated with Claude Code