Repository navigation
feat(validator): add the sendspin-validate skeleton with handshake and hello checks - #163
Merged
chrisuthe merged 7 commits intoOct 14, 2026
Conversation
…iosendspin 0002-noncompliance-observer.patch gives SendspinServer an optional noncompliance_observer, called with every spec violation the server flags in a client, before it tolerates the violation or rejects the client. Each report carries the reason, the client id and the connection number the frame observer uses. The client validator needs it to report why a strict server rejected a client without parsing log text. The bundled tree is rebuilt with vendor_aiosendspin.py 10.0.2.
The recorder is the bundled server's frame and non-compliance observer. It numbers each frame, decodes the JSON ones, keeps only the type and size of binary ones, and redacts pairing secrets (long_term_psk, wrapped_psk, wrapped_nonce_B, pake_msg_*, *_kc and registered tokens or codes) as it records, before anything reaches --verbose output or protocol.log.jsonl. A frame it cannot process is kept as a tool error instead of being raised into aiosendspin, which would swallow it. The Prompter interface (instruct, report_check, ask_text, ask_choice, confirm) is the steps' only channel to the user. The terminal prompter uses the standard library only, accepts s, r and ? on every question, and escapes control characters in client-supplied text. The auto-answer prompter is for CI. A skipped or unsure human answer maps to SKIPPED, never PASS.
Each check is a pure function of one recorded connection, with a stable id and the spec lines it enforces at 1.0.0-rc1: client/init fields (CORE-INIT-001..005), the Noise message 2 payload (CORE-NOISE-001), nothing but hello or goodbye before activation (CORE-ORDER-001), client/hello (CORE-HELLO-001..007, the TXT name check a WARN), the initial client/state (CORE-STATE-001..003) and client/time (CORE-TIME-001). Every rejection by aiosendspin's strict mode is CORE-STRICT-001, cited to the spec line of the rule it enforces. All 127 reasons the bundled copy can report are mapped. A rule rc1 does not make, or a reason the table does not know, is a TOOL_ERROR rather than a FAIL the spec cannot back. A check whose evidence never arrived because the connection had already been rejected is N/A, and a check that raises is a TOOL_ERROR.
sendspin-validate runs a strict bundled-aiosendspin server on port 8927, advertised as _sendspin-server._tcp, and gets one client connected: the app connects in, or the validator connects to a _sendspin._tcp client the user picks from an mDNS browse, or to --connect ws://host:port/path. It admits the client through unpaired access, explains when the app has that disabled, watches the opening exchange through the initial client/state and time sync, then judges the recording. The run writes report.json (sorted, with the tool version, spec tag, bundled aiosendspin commit and patches, seed, connection mode and client identity), a self-contained report.html and protocol.log.jsonl, and prints a summary. The Noise identity and pairing store live in the platform user-data directory; --fresh replaces them. The self-test runs the real command against the bundled aiosendspin client in both directions and requires no FAIL or TOOL_ERROR.
…connection A server-initiated dial now declares playback, so the validator wins admission from a server that holds the client idle; with the discovery reason the client kept the other server and answered concurrent_attempt. Observation ends as soon as the validator answers the opening frame with server/error or aiosendspin redials, instead of waiting out the step timeout while it reconnects. The client's goodbye is reported however observation ends. A self-test case drives a client that skips the handshake and expects a CORE-INIT-001 FAIL and exit status 1.
In server-initiated mode the validator now only dials and does not listen, so no other client can reach it; in client-initiated mode the first connection is the one judged, and any other is named as not judged. Its identity comes from that connection alone. A listen failure is reported as a port problem only where the listener starts. Discovery shows services it cannot connect to, with the reason, and resolves them concurrently. A Noise message 2 rejection counts once, under CORE-NOISE-001. Waits wake on recorded events instead of polling, file IO runs off the event loop, a failing check logs its traceback, and the user is told before the validator takes a client over from another server. The unpaired-access store keeps only the approval it needs. The refs test now fails when the bundled copy changes its composed reasons or when a reason matches more than one pattern.
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.
Phase 1 of the client validator (spec §2, §5.1, §7–§9).
sendspin-validateis a strict Sendspin server, built on the bundled aiosendspin, that a client app connects to. It records every frame, runs the first machine checks, and writes a report.Part of Sendspin/backlog#84.
What's in it
0002-noncompliance-observer.patch:SendspinServer(noncompliance_observer=...)reports every spec violation the server flags, with the connection number the frame observer uses, so the validator learns why a strict server rejected a client without parsing log text. It is upstream-shaped, with its own tests, and aiosendspin's suite passes with it applied (2972 tests). The validator spec's patch list now reads 0001 frame observer, 0002 non-compliance observer, then raw send, pairing driver and forget-client.--port), announced as_sendspin-server._tcpwithpath=/sendspin._sendspin._tcp, lists what it finds (including services it cannot connect to, with the reason), and dials the one the user picks.--connect ws://…skips mDNS.allow_noncompliant_clients=False, and every client is admitted through unpaired access.playback, so the validator wins admission over a server that holds the client idle. The user is warned first.long_term_psk,wrapped_psk,wrapped_nonce_B,pake_msg_*,*_kcand registered tokens or codes as each frame is recorded. It writesprotocol.log.jsonl, and--verbosestreams frames to stderr.instruct,report_check,ask_text,ask_choiceandconfirm, in a stdlib terminal implementation. Every prompt acceptss,rand?; skip maps to SKIPPED. A hidden--auto-answerflag is for CI.CORE-INIT-001..005: client/init fields.CORE-NOISE-001: Noise message 2 payload.CORE-ORDER-001: ordering before activation.CORE-HELLO-001..007: client/hello. The TXT name check is a WARN.CORE-STATE-001..003: client/state.CORE-TIME-001: client/time.CORE-STRICT-001: every strict-mode rejection, cited to its rc1 spec line. All 127 reasons are mapped. A rejection that rc1 does not back is a TOOL_ERROR, not a FAIL.report.json(sorted, recording the tool version, spec tag, bundled aiosendspin commit and patches, seed, mode and client identity), a self-containedreport.htmlandprotocol.log.jsonl, all written to./sendspin-validate-<client>-<timestamp>/.Testing
sendspin-validate --auto-answeragainst the bundled aiosendspin client:CORE-INIT-001and exit with status 1.ci.yml's unit-test step. 449 tests pass on Python 3.12, and the vendor--checkpasses.05f9944, headless (-o null), in server-initiated mode, picked from the validator's own mDNS list:--allow-unpaired: 19/19 PASS.concurrent_attemptbecause another server on the LAN already held it, which is correct per spec.Notes for review
publish.ymlruns once on this PR. Thesendspin-validateentry inpyproject.tomlis on its path filter. No harness code changed.