feat(local-link): generic handshake, activity messages (#1119) #5
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/1119-plugins-split-rkd-local-link-handshake-fr"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
This is part of nanoya-biz/rkd#1119 (split from #578; the design is ADR-0005, decision 3).
rkd-local-link's handshake is now generic over each plugin's own messages, and it adds the messages foractivity.game.watch@1, which the futurerkd-activityplugin will speak. Terminal messages are unchanged on the wire, and no release is tagged here.msg::ClientMsg<M>andmsg::PluginMsg<M>carry hello/auth/pair/pair_begin/unpair and hello/ready/paired/pair_code/error. A plugin's own messages sit in aPlugin(M)variant, andFrom<M>wraps them.msg::terminal::{ClientMsg, PluginMsg, Dir, DirKind}. None of the activity variants were added to the terminal's enum.msg::terminal::{PROTOCOL = 2, MIN_PROTOCOL = 1}. Activity starts atmsg::activity::{PROTOCOL = 1, MIN_PROTOCOL = 1}. The crate-rootPROTOCOL/MIN_PROTOCOLare gone.Plugin(m)serializes exactly asmdoes: one flat{"type":…}object. On read, thetypedecides whether a message is handshake or plugin, and that half gives the error. With serde's untagged-variant fallback, every malformed message would have been reported as "data did not match any variant". See the wire transcript below.msg::activity), idactivity, capabilityGAME_WATCH = "activity.game.watch@1":watch { capability }andunwatch { capability }.activity { activity: {kind:"game", name, steam_app_id, started_at} | null }.nullmeans nothing is running and is never omitted.steam_app_idis required (a record without one is refused);nameis capped atMAX_NAME_CHARS= 128.started_atis Unix seconds.MIN_INTERVAL(5 s). It sends the current value right afterwatch, and it does not scan while no watch is open.pluginfield (handshake/terminal/activity). The existing terminal vectors are unchanged byte for byte.proofis the proof vector's) and 5 activity vectors (non-ASCII name, missingsteam_app_idrefused,null).tests/vectors.rschecks that each vector round-trips under the enums it belongs to, and handshake vectors under both plugins'.local/README.md"Changing the contract" now explains per-plugin protocols and has a table per plugin.Breaking for Rust users of the crate, which means the RKD Tauri shell when it moves its
tag = "v0.4.0"pin:ClientMsg/PluginMsgtake a type parameter,Dir/DirKindmove tomsg::terminal, andPROTOCOL/MIN_PROTOCOLmove tomsg::terminal. The wire does not change, so an app on v0.4.0 keeps working with a plugin built from this.Type of change
Screenshots / recording
This is a contract change with no UI, so the evidence is a wire transcript. The same scripted Python client drove two
rkd-terminal servebinaries over a real socket: BEFORE is built from tagv0.4.0(a detached scratch worktree), and AFTER is built from this branch. The client pairs, then sendsopenwithargv, anopenwith a missingcwd,open/resize/close,dirs, a secondhello, an activitywatchsent to the terminal, adirsbeforeready, andhellowith protocol 3. It prints every control frame in both directions as raw JSON. Only random values are masked (nonces, code, client id, secret, version, home path), and stream output is skipped because its timing varies.diff BEFORE AFTERshows one line. Every frame is identical except the free-textmessageof theunexpectederror for an unreadable message: it used to list the handshake types as well as the terminal's, and now lists the terminal's alone.typeandcodeare unchanged.messageis for people and not part of the contract.BEFORE: rkd-terminal v0.4.0
AFTER: this branch
Test plan
Automated:
cargo fmt --all --checkcargo clippy --workspace --all-targets --locked -- -D warnings(macOS host)cargo clippy --target x86_64-pc-windows-msvc --workspace --all-targets --locked -- -D warnings(the CIwindows-checkjob)cargo test --workspace --locked: local-link 35 unit + 3 vector tests, terminal 86 unit + 1 + 31 integration testsnullvs omitted, unknownkindrefusedskip_serializing_ifonsteam_app_id, and droppingunpairfrom the handshake routing listManual:
Review checklist
dbg!or commented-out code left behindNotes for the reviewer
app/src-electron/localLink.ts,fakePlugin.test.helper.ts, the vendoredtest-vectors.jsoncopy, and the Taurirkd-local-linkpin bump with itsClientMsg<terminal::ClientMsg>port. The Tauri shell pinsrkd-local-linkby tag, and the Electron copy of the vectors is "a copy of vX.Y.Z", so it needs a tag to point at. It also builds on nanoya-biz/rkd#1125 (#1118,LINK_TARGETS), which is still open.started_atis Unix seconds.steam_app_idis required (review819d52b): the server'sActivity::Gamecarriessource: steam, so a non-Steam launcher needs a new source field and a protocol bump anyway.watchanswers with the current value at once, and a repeatedwatchresends it.watchof a capability the plugin does not provide getserror { unexpected }, and the connection stays up.rkd-activityitself (the detector) is not part of this. This PR defines only its messages.https://claude.ai/code/session_01S6WwLMTpMtw1RuCgBY8JGt
The handshake enums (hello/auth/pair/paired/ready/error, pair_begin, unpair) are now generic over a plugin's own messages, which move to msg::terminal and the new msg::activity (ADR-0005 decision 3), instead of the activity variants joining the terminal's enum. Protocol versions move with them: terminal keeps 1..=2, activity starts at 1, so one plugin's bump never makes the app call the other outdated. A plugin message is written as its own enum writes it, so terminal messages are byte-identical on the wire. Reading dispatches on `type` (handshake or the plugin's) rather than serde's untagged fallback, which would have reduced every parse error to "did not match any variant". activity.game.watch@1: watch/unwatch { capability } from the client, activity { activity: {kind: game, name, steam_app_id?, started_at} | null } from the plugin. test-vectors.json gains a `plugin` field on every message vector, handshake vectors and activity vectors. Breaking for Rust users of the crate: msg::{ClientMsg, PluginMsg} take a type parameter, Dir/DirKind move to msg::terminal, and the crate-root PROTOCOL/MIN_PROTOCOL become msg::terminal::{PROTOCOL, MIN_PROTOCOL}. Claude-Session: https://claude.ai/code/session_01S6WwLMTpMtw1RuCgBY8JGt