Repository navigation
Conversation
…abs#916) `signing.next_nonce()` is a process-local millisecond clock, so the built-in signer's nonces are only ordered within one process. Two Worker isolates holding one key are two unsynchronised generators, and a key that has ever signed at nanosecond scale sits above every value this clock will reach. In both cases say_signed, claim_room and set_room_allow returned a replay refusal for a write the server had just composed itself. Both refusals already name the floor: the message lane's 400 ("is not greater than P") and the ownership counter's 403 ("(last C)"). The built-in signer now parses that floor, raises its process floor past it, and re-signs, at most NONCE_RETRIES (2) times. External signatures are never re-signed: the caller chose that nonce and the refusal is its answer. A refused write did not land, so a retry cannot duplicate one. The parser is anchored at the start of the body, so a caller's own text echoed later in an error cannot trigger a re-sign. Tests: the new tests in tests/test_mcp.py provoke both refusals against the real app (three of them fail on main), check that an external signature is posted exactly once, and that a floor that keeps moving is chased a bounded number of times. Unit tests pin the parser to the service's exact wording. Verified against main@0e47f77; `uv run just check` passes (871 passed, 1 skipped). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FNEEGsnjfNgXGQNKZ1EE67
|
Open pull requests citing the same issues:
If one already covers this change, review or build on it instead of racing it (CONTRIBUTING.md "Overlapping work"). |
|
Issue-reporter check at head |
Independent validation at
|
What
The built-in MCP signer now recognizes the two server nonce refusals that name the current floor, advances its process-local nonce above that floor, and re-signs at most twice. Caller-supplied signatures are still attempted exactly once.
Why
Fixes #916. Two isolates sharing one key can issue nonces out of order, while a key previously used with nanosecond-scale nonces remains permanently above the wrapper's millisecond clock. In either case, the server refused writes that the built-in signer had composed itself.
The refusal parser is anchored to the start of the response, retries are bounded, and rejected writes cannot be duplicated because they never landed. This deliberately leaves external nonce handling unchanged; #685 touches the same tools for that separate concern and may require a small rebase depending on merge order.
Checks
uv run coverage run -m pytest tests -q && uv run coverage report- 871 passed, 1 skipped; 98.10% coverageuv run ruff check . && uv run ruff format --check .anduv run ty check