Cloud Backend for tobor (Mode A)¶
tobor is the small client that runs on each robot. It registers the robot with the cloud once, it can check that the robot can reach the cloud and that its stored credential is still accepted, and it can unregister to remove that credential when a robot is reset or decommissioned. Keeping a robot continuously connected so the cloud sees it online is the next step, not what it does today.
This doc lines up the robot side and the cloud side: how the pieces fit, where the binary comes from, and the parts still open. Paths and payloads below are a starting point from the robot side and can change to match whatever the cloud backend does. Mode A is the simple, ship-now design; there is a short note on the more secure Mode B at the end.
How the flow works¶
sequenceDiagram
participant Cloud
participant Operator
participant Robot as Robot (tobor)
Cloud->>Operator: 1. Issue a token for the robot
Operator->>Robot: 2. tobor register <token>
Robot->>Cloud: 3. POST /register (token + device identity)
Cloud-->>Robot: 4. 200 { "credential": "..." }
Note over Robot: 5. Robot saves the credential on disk
Robot->>Cloud: 6. tobor check: GET /health (reachable?)
Cloud-->>Robot: 7. 200 ok
Robot->>Cloud: 8. tobor check: POST /heartbeat (Authorization: Bearer credential)
Cloud-->>Robot: 9. 200 valid, or 401 rejected
- The cloud issues a token for a robot, and it reaches the operator.
- The operator runs
tobor register <token>on the robot. - The robot sends the token plus its identity to
/register. - The cloud checks the token and returns a persistent credential.
- The robot saves the credential on disk and reuses it.
tobor checkdoes a one-time check in two parts:GET /healthfor reachability, thenPOST /heartbeatpresenting the saved credential as a Bearer token, so the cloud can confirm the credential is still valid. Later this heartbeat becomes a continuous process (a reservedservecommand) that keeps the robot online.
The token bootstraps once. The credential is the durable secret the robot uses after that, and the heartbeat is where the robot presents it.
Where the binary comes from¶
The robot binary is built by the GitHub Actions workflows and published to the repo's Releases, so it can be pulled from there. Each release has two files:
tobor-<version>-aarch64.tar.gz, the binary (namedtoborinside).tobor-<version>-aarch64.tar.gz.sha256, a checksum to verify the download.
Since the repo is private, downloading uses a GitHub token with read access. Two ways:
- From the UI: open the repo's Releases page and download the two files.
- With a command (handy for a backend that fetches on a trigger):
gh release download --repo ToborlifeRobotics/tobor \
--pattern 'tobor-*-aarch64.tar.gz*'
gh handles the token. If a pure-curl fetch is easier for automation, call the Releases API with the token to read the latest release, then download the asset by its API URL with Accept: application/octet-stream.
Then verify and unpack:
sha256sum -c tobor-<version>-aarch64.tar.gz.sha256 # prints OK
tar xzf tobor-<version>-aarch64.tar.gz # gives ./tobor
A natural cloud-side setup: fetch the latest release on a trigger, verify it, and serve tobor to robots (for example a download URL the robot pulls from).
Running it on the robot (the operator)¶
Once the binary is on the robot, the operator registers it and checks it:
./tobor register <token> # one time, at setup
./tobor check # cloud reachable, and the stored credential still valid
On success, register saves the credential to /var/lib/tobor/credential on the robot and reuses it after that. check reads that credential to run its heartbeat.
The cloud URL is set in the code for now (with an environment override used for testing), and a config file will come later so it can point elsewhere without a rebuild. The only thing needed from the cloud side is the URL itself.
The register step¶
Token plus device identity in, persistent credential out. Below is what the robot sends and expects; the shapes can change to match whatever the cloud backend returns.
Request body the robot sends:
{
"token": "<the token>",
"device": {
"serial": "1424325095622",
"machine_id": "5dbfb12414a3456d9014d88183e338b1",
"os_release": "PRETTY_NAME=\"Ubuntu 22.04.5 LTS\"\n...",
"model": "NVIDIA Jetson Orin NX ..."
}
}
serialis the robot's permanent hardware serial. It survives a re-flash, so it is a natural key to record a device under.machine_idmarks the OS install, so a re-image is visible (sameserial, newmachine_id).os_releaseandmodelare there for the record.
On success, the robot expects 200 with a credential:
{ "credential": "<persistent credential string>" }
The robot treats the credential as an opaque string, so its format is defined entirely by the cloud side. The exact response shape is for the cloud side to set; the robot will be built to read it. Whatever credential is issued here, the cloud must recognize it again at /heartbeat (below), since that is where the robot presents it.
On failure, the status code drives the robot's retry logic. The robot retries on 5xx and network errors, and stops on 4xx. So a bad or missing token fits a 4xx (the robot stops, the operator fixes it), and a real outage fits a 5xx (the robot waits and retries). A starting point: 400 for a malformed or incomplete body, 401 for an invalid token.
Staying online (the next step)¶
tobor check now presents the credential. Its second step is a POST /heartbeat with an Authorization: Bearer <credential> header, and the cloud answers 200 if the credential is valid or 401 if it is not. That is a one-time check, run on demand (by an operator or a script).
The always-on version, where the robot pings on a schedule so the cloud can mark it online with a last-seen time, is the next step (a reserved serve command). The one-time heartbeat already built is the same call serve will make on a loop. The contract is still open:
- The ping cadence, and how long after the last ping a robot counts as offline.
- Anything wanted in the ping body, or in the reply. Today the robot sends an empty JSON body (
{}) and the credential rides in the header. - Which non-
200statuses mean the credential is rejected. Today the robot treats401as rejected and any other non-200as an unexpected error; if403(or another code) should also mean "re-register", the cloud side sets that.
A small status command is also under consideration, so an operator or a script can ask a robot to report cloud reachability and credential validity on demand. Open question before adding it: is it useful, or a risk? Many robots checking often could look like or add up to DDoS-style load. If the always-on serve process holds the connection, a status command could ask that local process instead of the cloud, so many status checks add no cloud load; whether it is worth having, and at what cadence and rate-limit, is worth deciding before it ships.
Decommissioning a robot (unregister)¶
tobor unregister removes a robot's credential when it is reset, resold, or decommissioned. It does two things: it asks the cloud to revoke the credential, then it deletes the local credential file. The local delete is unconditional (so a robot already off the network can still be wiped); the cloud revoke is best-effort.
The robot sends POST /unregister with the credential in an Authorization: Bearer <credential> header and an empty JSON body, the same shape as /heartbeat. The cloud revokes that credential and answers:
200if it recognized the credential and it is now revoked.401if it does not know the credential.
After a 200 the credential must no longer be accepted anywhere: a later /heartbeat with it returns 401. The robot treats any non-200 (including 401, unreachable, or a TLS failure) as "could not confirm", reports it, and still deletes the local copy.
Requirement: credentials must be fresh and never reused. Revocation only means something if a revoked credential can never come back. The real cloud must issue a fresh, unguessable credential on every /register, and must never reissue a revoked one. (The mock derives the credential from the serial, tobor-cred-<serial>, for test convenience, so re-registering reissues the same string. That is a mock simplification, not the contract.)
Planned refinement (not built yet, see DECISIONS.md D-0020). To support a strict client mode and make revoke idempotent, /unregister should return 200 for any credential this cloud issued, whether it is revoking it now or it was already revoked, and reserve 401 for a credential it never issued. That makes a repeated or interrupted revoke safe, and makes 401 a clear "wrong cloud or unknown credential" signal.
Transport (TLS)¶
Everything is HTTPS and the robot always verifies the certificate; there is no way to turn that off.
- A public-CA certificate works out of the box.
- A private or internal CA means the robot is pointed at that CA bundle at setup, so the cloud side just needs to say which is in use.
Things to confirm on the cloud side¶
- Tokens can be issued and delivered to the operator.
/registervalidates the token, records the device, and returns a credential in a defined shape./heartbeataccepts a credential presented asAuthorization: Bearer <credential>, answers200when it is valid and401when it is not, and records the device as recently seen.- Error responses follow the
4xxstop /5xxretry split. - A reachable endpoint returns
200when the service is up. - HTTPS is served with a cert the robot can verify (public or private CA, and which one).
/unregisteraccepts a credential asAuthorization: Bearer <credential>, revokes it, and answers200(revoked) or401(unknown); after a revoke,/heartbeatwith that credential returns401.
Open questions¶
- Token: how is it minted and delivered? Does it expire? Single use, or reusable? What happens when a robot re-registers (for example after a re-flash), a new credential or the existing one?
- Credential: does it expire or rotate? Revocation now has an endpoint (
POST /unregister, see Decommissioning). Two parts stay open: the real cloud must issue fresh, unguessable, never-reused credentials for revocation to mean anything, and the idempotent200-for-already-revoked behavior (DECISIONS.mdD-0020) is still to be built cloud-side. If a credential goes dead, the robot sees a401at/heartbeatand reports it rejected; should it re-register to recover? - Staying online: the cadence, the offline timeout, and the schedule for the always-on
serveversion. - Auth failure codes: only
401is treated as "credential rejected" today; confirm whether403or other codes should be too. - Status command: worth adding, or a DDoS risk to avoid?
- Paths and payloads: the endpoint paths, and the exact request and response shapes, so the robot side can match them.
Heads-up: Mode B is coming (short)¶
Mode A is a stopgap. It relies on shared secrets: the token and the credential are bearer secrets, so whoever holds one can act as the robot, and the credential sits on the robot protected only by file permissions. That is fine for an early, controlled rollout, but not for the long term.
Mode B will replace it: each robot makes its own private key, the CA signs it after owner approval, and the robot connects with mutual TLS. The key never leaves the robot, and identity becomes cryptographic instead of a shared secret. Nothing to build now. The one thing worth keeping in mind is to leave the device registry and auth flexible enough that moving to per-robot keys later is an addition rather than a rewrite.