Skip to content

Receiver consent: preview the transfer, write nothing until accepted - #72

Open
op-q wants to merge 4 commits into
feat/meta-ok-confirmationfrom
feat/receiver-consent
Open

op-q wants to merge 4 commits into
feat/meta-ok-confirmationfrom
feat/receiver-consent

Conversation

@op-q

@op-q op-q commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Consent plan, phases 2–3, recorded as decision 19. It changes behaviour and the wire (still protocol v2, unreleased), so it waits for your review. Stacked on #71.

What a receiver sees now

Incoming transfer
  Name     quarterly-report.pdf
  Type     PDF document (.pdf)
  Size     2.4 MiB
  Save as  ./quarterly-report-1.pdf   (instead of quarterly-report.pdf)
Accept? [y/N]

Folders show the file count and unpacked size. Programs (.exe, .ps1, .dmg, .sh…) get a warning line. The type comes from the real extension, not the sender's MIME type, so invoice.pdf .exe shows up as a program.

  • Nothing is created until accept. The destination is worked out by looking: numbering past existing names, plus the Windows rewrite from Windows receiver: rewrite names Windows would misread, skip what it can't store #70. A decline leaves the directory byte-identical.
  • An unanswered question is declined after 120s.
  • No terminal and no --yes → refuses before contacting anything, so the code isn't spent (your choice from earlier). Scripts piping drop recv need --yes; netlab and scripts/dev-transfer.sh are updated.

Sender

Prints what the receiver is doing: "The receiver entered the code and is deciding whether to accept...", "Accepted.", "The receiver has everything and is finishing up...". It also reports "the receiver declined the transfer", "…did not answer within two minutes…", and receiver cancels, with reasons.

Relay

  • New receiver frames: accept, decline {reason}, cancel {reason}, finishing. A sender cancel now reaches the receiver as cancel, not error: sender cancelled.
  • No chunk is carried before accept, and accept is refused before meta.
  • Reasons come from a fixed set and are normalised, so a peer can't put its own text on the other terminal.
  • /metrics gains total_transfers_declined and total_transfers_cancelled. These are no longer counted as failures.

Found while building it

While the question is open, the receiver must keep reading the socket, or the relay drops it for not answering pings. That read gets abandoned when the person answers, so receiving must be cancel-safe, and the QUIC framing wasn't: read_exact into a local buffer loses a partial frame when dropped. It now keeps partial frames in the transport, and the trait documents the requirement.

Tests (240, up from 217)

  • Relay: early-chunk gate, decline, a reason outside the set, cancel both ways, accept before meta, finishing.
  • Consent: timeout (paused clock), sender cancel mid-question, chunk before consent, relay narration, rendering, and the three clocks in order (120s < 150s < 300s session TTL).
  • End to end: a decline over a real relay leaves the destination byte-identical and counts as declined, not failed. A receiver with no terminal refuses without ever connecting to a listening socket.
  • Negative controls: removing the relay gate fails the early-chunk test; creating a file on the decline path fails the decline test; the old framed reader fails the cancel-safety test.
  • netlab against the real binaries: the relayed, UDP-blocked and load-bearing topologies pass. Plain LAN hit the separate, already-known intermittent QUIC authentication failed handshake error, which also occurs on main (details in the netlab plan).

Not in this PR (phase 4)

Letting a person cancel (a key, or a first Ctrl-C), drop-status: state= lines, and exit codes 3/4.

🤖 Generated with Claude Code

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
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

op-q and others added 2 commits September 15, 2026 12:22
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
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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant