sp-rtk-portal-protocol
The agent-to-Portal protocol, published from the sp-rtk-portal repo so the Portal and sp-rtk-base's Portal agent share one definition: message shapes, request signing and verification, tunnel framing, and the protocol version.
What's in it
PROTOCOL_VERSION(an integer, starting at 1, independent of either side's release version) andVERSION_HEADER(SP-RTK-Protocol-Version), sent on every agent request. Additive changes don't bump it: both sides ignore fields they don't know.- Request proofs (
proof.py). The agent's Ed25519 base key (BaseKey) signs every request with a DPoP-style JWS (RFC 9449), sent asAuthorization: Signature <proof>: header{"typ": "dpop+jwt", "alg": "EdDSA", "jwk": <public key>}, claimshtm,htu(URL without query),iat(seconds),jti,bh(base64url SHA-256 of the body, when there is one) and optionalnonce.verify_proofchecks the signature, the request it was made for, the body and thatiatis within 60 s;ReplayCacherefuses ajtiseen before. The Portal identifies a base by the key's RFC 7638 thumbprint and stores only the public key. - Message shapes (
messages.py): frozen dataclasses withto_json()/from_json().from_jsonignores unknown fields and raisesMessageErrorfor a missing or mistyped one.- Claim (RFC 8628):
POST /agent/claimswithClaimStart→ClaimStarted;POST /agent/claims/tokenwithClaimToken→Claimed, or HTTP 400ErrorReply(authorization_pending,slow_down,expired_token,access_denied,invalid_grant). Both requests are signed with the new key, so only the key that started a Claim can collect it. - Polls and Check-ins (a Claimed base, signed with its key):
POST /agent/pollwithPoll(state)→PollReply(config, metered, notices);POST /agent/check-inwithCheckIn(software_version, doorbell, lan_addresses, paused)(pausedfrom 0.9;Nonefrom older agents) →CheckInReply(config, metered, notices).configis the revisionedAgentConfigblock (poll intervals, Check-in interval, Doorbell host and port, tunnel URL, tunnel timeouts; the agent applies it only on a new revision). An unknown key gets 401ErrorReply("unclaimed"). - Release (a local Un-claim, signed with the base key):
POST /agent/releasewithRelease()→Released(base_id), or 401unclaimedif the Portal already forgot the key. Best effort: the agent wipes its key either way. - An agent below the Portal's minimum protocol version gets HTTP 426
UpgradeRequired(min_version, message)on any request. - Messages may nest messages, lists of them and optional (
X | None) fields. - A refused proof is HTTP 401
ErrorReply("invalid_proof"). - A Wake:
PollReply.wakecarries a one-time nonce (and nothing else). PollReply.sessions: the base's live Remote sessions (LiveSession(who, since, shared)), for its local Portal page.Poll.stateispausedwhile remote access is paused on site; a paused agent ignores Wakes.
- Claim (RFC 8628):
- Doorbell (
doorbell.py). UDP packets between a base and a Doorbell server: a keepalive signed by the base key (keepalive,read_keepalive,verify_keepalive; 109 bytes) and a ring carrying a Wake's nonce, authenticated with the base's ring key (ring,open_ring,new_ring_key); both refused more than 60 s from now.DOORBELL_INTERVAL(20 s) andDOORBELL_JITTER(5 s).CheckInReply.doorbell_heardand.ring_keytell the agent the Portal hears its keepalives and give it the ring key. The Doorbell server's HTTPS interface on the Portal:KEEPALIVES_PATH(KeepalivesReport→KeepalivesReply) andRINGS_PATH(→Rings); see the Portal README. - Tunnel (
tunnel.py). The agent openswss://<Portal host>/agent/tunnelwith a GET signed forhttps://<Portal host>/agent/tunnel(signed_url) whose proof'snonceclaim echoes the Wake. Each binary websocket message is one frame: a type byte, a big-endian 4-byte stream id, then the payload. Types:NEW(1, no payload ever: a stream never names a destination),DATA(2, 1..64 KiB),CLOSE(3),PING/PONG(4/5, stream 0),PAUSE/RESUME(6/7, flow control). An unknown type, a payload where none belongs, or aNEWfrom the agent raisesFrameError, and the receiver closes the whole tunnel.Tunnelis one end of it (independent of the websocket library: give it asendcoroutine andfeedit each message); only the Portal end opens streams (open_stream()->Streamwithread/write/close).
The package version (pyproject.toml) is bumped on every change, so a pinned consumer can tell which shapes it has.
It is published to PyPI, and sp-rtk-base depends on it from there:
[project]
dependencies = ["sp-rtk-portal-protocol>=0.9.0,<1"]
Raise the lower bound when the agent needs newer shapes. The Portal uses this directory directly, as a uv workspace member.
For local development of both sides at once, point sp-rtk-base at a checkout next to it with [tool.uv.sources] sp-rtk-portal-protocol = { path = "../sp-rtk-portal/protocol", editable = true }, and don't commit that.
Releasing
.github/workflows/protocol-release.yml publishes this package to PyPI when a protocol-v<version> tag is pushed. It checks the tag against version in this pyproject.toml, runs these tests on Python 3.10 to 3.13, builds with uv build, runs twine check, then uploads with PyPI Trusted Publishing (OIDC, no token). Portal image tags (v*) and protocol tags don't trigger each other's workflow.
Once, before the first release:
-
On pypi.org, under Your account > Publishing > Add a new pending publisher > GitHub, enter:
- PyPI project name:
sp-rtk-portal-protocol - Owner:
rodenj1 - Repository name:
sp-rtk-portal - Workflow name:
protocol-release.yml - Environment name:
pypi
- PyPI project name:
-
On GitHub, in the sp-rtk-portal repo's Settings > Environments, create an environment named
pypi(optionally with required reviewers, to approve each upload).GitHub offers environments in a private repo only on a paid plan (Pro, Team or Enterprise). If Settings has no Environments page, leave the environment name blank in step 1, and delete the
environment:block from the workflow'spublish-pypijob.
Each release:
-
Bump
versionin thispyproject.tomlonmain(PyPI never accepts the same version twice). -
Tag that commit and push the tag:
git tag protocol-v0.9.0 git push origin protocol-v0.9.0
-
Once it's on PyPI, raise sp-rtk-base's lower bound if the agent needs the new version, then run
uv lockthere.
To rerun a failed release for an existing tag, run the workflow by hand (Actions > protocol-release > Run workflow) with the tag.
Metadata
Release files for sp-rtk-portal-protocol 0.9.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sp_rtk_portal_protocol-0.9.0.tar.gz | 17.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sp_rtk_portal_protocol-0.9.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 37.8 kB
Release files / sp_rtk_portal_protocol-0.9.0.tar.gz
| Download URL | sp_rtk_portal_protocol-0.9.0.tar.gz |
|---|---|
| Size | 17.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bb4c6dc4f1001bfd6a698ab29a09431aebaa733052c6b8c51a285a794b91901b
|
|
BLAKE2b-256 checksum How to use checksums |
1d283fee12039981326f4d8e9a500109cba61ed5ec2ee33a09701afbbb5ec929
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.12.9
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.
Transparency logRelease files / sp_rtk_portal_protocol-0.9.0-py3-none-any.whl
| Download URL | sp_rtk_portal_protocol-0.9.0-py3-none-any.whl |
|---|---|
| Size | 20.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7d82c21f3c9a412e67c416ba2ef6f8324057b79ff8fc42e53cc30d6f0aa1013b
|
|
BLAKE2b-256 checksum How to use checksums |
c975f52f1130c48169041863bacde3fe702e7f2deea97c526d1d29b28a2046cf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.12.9
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.
Transparency log