Changelog¶
All notable changes to tobor are recorded here.
Format follows Keep a Changelog. tobor
follows Semantic Versioning, as documented in the project ../README.md.
Discipline. Every merged PR adds at least one entry under [Unreleased]. On release, [Unreleased] is renamed to
the release version with the release date, and a fresh [Unreleased] section is created for the next cycle. This is one
of the three sources of truth for tobor's version, alongside ../CMakeLists.txt (project(... VERSION ...)) and
tobor --version; all three must agree at release.
Section headings, in order (only used sections are shown per release):
- Added - new features and capabilities.
- Changed - changes to existing behavior.
- Deprecated - features scheduled for removal.
- Removed - features that were removed.
- Fixed - bug fixes.
- Security - vulnerability fixes.
Task references. Each entry references the BACKLOG.md task ID (e.g. TV-16) that produced it. The Monday.com item
ID lives on the PR and in the commit trailer, not here.
[Unreleased]¶
Added¶
unregisterdecommissions a robot (TV-XX). Newtobor unregisterremoves the stored credential so a robot can be reset, resold, or decommissioned. It asks the cloud to revoke the credential first (POST /unregisterwith the credential as a Bearer token), then deletes the local file; the delete happens even when the cloud is unreachable or does not recognize the credential, so an offline decommission still works, and the command says when a revoke could not be confirmed. With no credential stored it is a friendly no-op. It shows the exact path and removes the file only on a typedyes;--forceskips the prompt for automation. Only the credential file is removed (/var/lib/toborstays), so re-registering needs no root. Revoke-before-delete is also crash-safe. SeeDECISIONS.mdD-0019.remove_credentialandcredential_pathin the store (TV-XX).remove_credentialdeletes the credential and returns true when the file is gone (deleted or already absent);credential_pathexposes the resolved path so a command can name the file without duplicating the default.save_credentialandload_credentialnow resolve the path throughcredential_pathtoo. SeeDECISIONS.mdD-0019.send_revoketransport and mock/unregister(TV-XX). A newsend_revokeposts to/unregisterwith the credential as a Bearer token, mirroringsend_heartbeat. The mock cloud implements/unregister: it forgets the credential it issued, so a later/heartbeatwith that credential returns401. In-memory only, like the rest of the mock. SeeCLOUD_BACKEND.md.checkvalidates the credential (TV-XX).check(formerlyrun) now does two things: it confirms the cloud is reachable (GET /health), then sends a one-shot heartbeat (POST /heartbeat) with the stored credential as a Bearer token so the cloud can confirm the credential is still accepted. Success prints two lines; it exits non-zero with a clear message if the cloud is down, the TLS certificate fails, the robot is not registered, the credential cannot be read, or the credential is rejected (401). The one-shot heartbeat is the call the futureservecommand will make on a loop. SeeDECISIONS.mdD-0016.load_credentialsays why a read failed (TV-XX). It now returns a status (Loaded,Missing,Unreadable, orEmpty) instead of just the text or nothing, socheckcan tell "not registered" apart from a permissions problem, a directory path, or an empty file, and report each one plainly. SeeDECISIONS.mdD-0018.- Shared HTTP result classification (TV-XX). A new
HttpOutcomeenum and aclassify()function interpret an HTTP result in one place (TLS failure, unreachable,200,401, other), soregisterandcheckshare the logic instead of each repeating it. SeeDECISIONS.mdD-0018. - Command table for the CLI (TV-XX). Commands are declared in one table (name, help line, handler), so
--helpand dispatch stay in sync and adding a command is one line. SeeDECISIONS.mdD-0017. - mock-cloud validates heartbeats (TV-XX).
/registerrecords the credential it issues, and/heartbeatchecks the Bearer credential, returning200for a known one and401otherwise, so both paths can be tested. In-memory only, so restarting the mock forgets issued credentials. - GitHub Actions CI for the aarch64 target (
.github/workflows/aarch64-build.yml) (TV-22). Builds the Release aarch64 binary on a nativeubuntu-22.04-armrunner on every merge to main (plus a manual trigger), ending with a--versionsmoke test. Runs only on merge to keep cost minimal, and uses the 22.04 arm image so the toolchain matches the robot (D-0001, D-0003). Tooling only, no change to the shipped binary, so the version stays 0.4.0. Rationale inDECISIONS.mdD-0014. - Release workflow publishes the aarch64 binary (
.github/workflows/release.yml) (TV-23). Pushing a version tag (v*.*.*) builds the Release aarch64 binary onubuntu-22.04-arm, checks the tag matches the binary's version, and publishes a GitHub Release with a checksummedtobor-<version>-aarch64.tar.gz(the binary namedtoborinside). Also: the build workflow now skips doc-only merges (paths-ignore) and both workflows log the toolchain versions and build verbosely. Tooling only, version stays 0.4.0. Rationale inDECISIONS.mdD-0015. - Dev tooling: the mock cloud takes a
--hostflag (default127.0.0.1) so it can bind0.0.0.0and answer other machines, which lets you smoke-test a release binary on the robot against a mock running on your laptop. - Docs: add
docs/CLOUD_BACKEND.md, the cloud-side integration spec for Mode A (endpoints, the register and online-signal flow, and open questions). Docs only, version stays 0.4.0.
Changed¶
- Shared cloud-URL resolution (TV-XX). The
env_or("TOBOR_CLOUD_URL", ...)lookup, now used byregister,check, andunregister, is factored into a singlecloud_url()helper incommands.cpp(the third caller, by the rule of three), matching the existingenv_or/credential_pathconsolidation. runrenamed tocheck(TV-XX). The connectivity command is nowcheck;tobor runreportsunknown command. A CLI-contract break, which pre-1.0 is a MINOR change, reflected in the README andCLOUD_BACKEND.md. The nameserveis reserved for the future always-on online signal. SeeDECISIONS.mdD-0016.- mock-cloud credential format (TV-XX). The issued credential is now
tobor-cred-<serial>instead of the fixedtobor-cred-CHANGED-2026, so different robots get distinct credentials the heartbeat can validate. Dev-only.
[0.4.0] - 2026-09-01¶
Added¶
- HTTPS/TLS for the Mode A transport (
../src/http_client.cpp,../include/http_client.hpp) (TV-21). The transport now uses TLS whenever the cloud URL ishttps://(plainhttp://is unchanged), driven entirely by the URL scheme. Certificate verification is always on and set explicitly (CURLOPT_SSL_VERIFYPEERandCURLOPT_SSL_VERIFYHOST); there is no switch to disable it. A new optionalTOBOR_CA_BUNDLEpoints libcurl at a custom trust anchor (CURLOPT_CAINFO) for a self-signed dev cert or a private CA; unset, tobor uses the system CA bundle, so the real cloud needs no configuration (the robot needsca-certificatesinstalled). tobor presents no client certificate: this is one-way TLS, and mutual TLS remains a Mode B concern. Rationale inDECISIONS.mdD-0013. - TLS trust failures are terminal and reported honestly (
../src/commands.cpp,../include/http_client.hpp) (TV-21). The transport surfaces a TLS trust failure as a distinct outcome (HttpResult.tls_verify_failed, set fromCURLE_PEER_FAILED_VERIFICATIONwhen the cloud's certificate is not trusted, orCURLE_SSL_CACERT_BADFILEwhen theTOBOR_CA_BUNDLEfile cannot be read).is_retryabletreats it as terminal, soregisterno longer retries a bad certificate or a mistyped CA path four times, and bothregisterandrunreport that the cloud's TLS certificate could not be verified instead of the misleading "couldn't reach the cloud". The transport reports the fact; the commands decide and phrase it, keeping the D-0008 split. - mock-cloud can serve HTTPS (
../mock-cloud/server.py) (TV-21). The mock takes--tls,--port,--cert, and--keyflags and wraps its socket with a self-signed certificate for local TLS testing; plain HTTP on 8080 is unchanged when the flags are omitted. Responses now send aContent-Lengthheader so a TLS client reads a complete body instead of failing on the connection close. The dev cert and key are generated from a documentedopensslcommand and are git-ignored.
[0.3.0] - 2026-09-01¶
Added¶
- Retry with backoff on
register(../src/commands.cpp) (TV-20).cmd_registernow wraps the one-shotregister_robotin a small retry loop that retries only transient failures - a transport failure (could not connect or timed out) or a 5xx - and stops at once on any settled answer (200, 401, other 4xx, or a credential-save failure). The retryable/terminal split is a privateis_retryable(HttpResult)helper. Default policy: 4 attempts (first try plus 3 retries) with delays of 1s, 2s, 4s (base 1s, doubling), about 7 seconds worst case, printing a visibleretrying (n/N) in Xs...line before each wait. The transport stays one-shot and protocol-neutral; the loop and all its user-facing messages live in the command layer, preserving the D-0008 split. Retry is safe because the cloud keys the device on its hardwareserial(D-0011), so a repeat updates the same record. Rationale inDECISIONS.mdD-0012. env_int_orhelper and two retry env knobs (../include/util.hpp) (TV-20). New header-onlyenv_int_or(name, fallback, min, max)reads an integer from the environment, falling back to the default when the variable is unset, non-numeric, or out of range.cmd_registeruses it to exposeTOBOR_REGISTER_MAX_ATTEMPTS(1 to 10, default 4) andTOBOR_REGISTER_RETRY_DELAY(0 to 60 seconds, default 1), following the D-0005 runtime-config pattern. The bounded maximum also keeps the doubling delay from overflowing anint.
[0.2.0] - 2026-09-01¶
Breaking: the /register request body changed from a bare token to a JSON object. Pre-1.0, breaking changes bump the
minor version (see ../README.md "Versioning").
Added¶
- Device identifiers gathered for registration (
../src/device_info.cpp,../include/device_info.hpp) (TV-19). Newcollect_device_info()returns aDeviceInfowith the robot's hardwareserial(/proc/device-tree/serial-number),machine_id(/etc/machine-id), rawos_release(/etc/os-release), and boardmodel(/proc/device-tree/model). Each is read defensively - a missing file yields an empty string, not an error - so one binary runs unchanged on the aarch64 robot (device-tree present) and the x86 dev box (device-tree absent). Theserialis overridable viaTOBOR_DEVICE_SERIALfor dev machines that have no device-tree. Anchor choice and the send-raw-os_releaserationale inDECISIONS.mdD-0011. - mock-cloud
/registeraccepts the JSON payload (TV-19). The mock server parses the JSON body, requires bothtokenandserial(missing either produces a400), validates the token (401on mismatch), records each device byserialin an in-memory registry, and returns the credential on success. Stands in for the cloud's device database.
Changed¶
/registerrequest body is now JSON (TV-19), a breaking change to the request contract, coordinated with the cloud team. The body was the raw token string; it is now{"token": "...", "device": {"serial": ..., "machine_id": ..., "os_release": ..., "model": ...}}, withtokenandserialrequired. The response (a JSONcredential) is unchanged.cmd_registerbuilds the payload withnlohmann/jsonanddump()s it before sending. SeeDECISIONS.mdD-0011.register_robot(base_url, body)(TV-19). The transport's second parameter wastoken; it is now a neutralbody. Payload construction stays in the command layer (cmd_register); the transport just posts the bytes it is given, keeping it protocol-neutral (D-0008).
[0.1.4] - 2026-08-31¶
Added¶
- Credential storage module (
../src/credential_store.cpp,../include/credential_store.hpp) (TV-19). Newsave_credential(credential)persists the credential from a successful/registerto disk, mirroring the transport seam: the module owns all file mechanics, andcmd_registerjust calls it and checks abool. The write is atomic: the credential is written to a.tmpsibling which is thenrenamed into place, so a crash or power loss can never leave a half-written or lost credential, making re-registration safe. The file is locked to0600and its directory to0700(owner-only), sized to the same uid the robot's containers run as, sotoborand the on-robot container can read it while other users cannot. The on-disk path defaults to/var/lib/tobor/credentialand is overridable via theTOBOR_CREDENTIAL_PATHenvironment variable, following the D-0005 runtime-config pattern. Storage rationale (location, permissions, plaintext-vs-encryption, container access) inDECISIONS.mdD-0010. registersaves the credential and no longer prints it (TV-19). On a200,cmd_registernow callssave_credentialand reportsSuccess!only once the credential is safely on disk; on a storage failure it reports the reason in plain language and exits non-zero. The temporary line that echoed the credential to stdout was removed, so the secret never reaches the terminal or the systemd journal.
Changed¶
- Shared
env_orhelper (TV-19). The duplicatedgetenv-or-default logic (a third copy appeared once the credential path was added) was factored into a singleenv_or(name, fallback)in../src/commands.cpp.cmd_registerandcmd_runuse it forTOBOR_CLOUD_URL; the credential path resolves the same way insidecredential_store.cpp, to be unified when the config file lands.
[0.1.3] - 2026-08-31¶
Added¶
registercommand (TV-19).cmd_registerobtains the per-robot token from a CLI argument (tobor register "<token>") or, if none is given, an interactive prompt ("Paste your registration token and press Enter:"); an empty token is a usage error (exit 2). It then sends the token to the cloud and reports the outcome in plain language:Success! Your robot is registered.on 200,that token wasn't accepted...on 401, and a can't-reach-the-cloud message on a transport failure. Token-input rationale inDECISIONS.mdD-0007.- JSON parse of the
/registerresponse (TV-19). On a200,cmd_registerparses the cloud's JSON body with the vendorednlohmann/jsonand extracts thecredentialfield, replacing the temporary raw-body print. A malformed body, a missingcredentialkey, or an empty credential is reported in plain language and exits non-zero; the parse is wrapped intry/catch, since nlohmann/json signals these by throwing. Parser choice and vendoring rationale inDECISIONS.mdD-0009. - HTTP POST transport (TV-19).
register_robot(base_url, token)sends the token to/registervia a libcurl POST (CURLOPT_POSTFIELDS) and returns the result. - mock-cloud
/registervalidation (TV-19). The mock server now checks the received token against a known-good value: a match returns200+ a JSON credential, anything else returns401+ an error, so both the success and rejection paths can be developed and tested against it.
Changed¶
- Transport returns a result; commands interpret it (TV-19). Introduced an
HttpResultstruct (ok/status/body).check_cloud_healthandregister_robotnow perform the request and return the raw result only;cmd_runandcmd_registerdo the judging (the 200 check) and all user-facing messages. Moves policy and presentation out of the transport layer. SeeDECISIONS.mdD-0008. - Shared HTTP request helper (TV-19). The duplicated curl setup across the two transport functions is factored into
one private
perform_request(url, post_body)- GET whenpost_bodyis null, POST otherwise - leavingcheck_cloud_healthandregister_robotas thin wrappers. libcurl stays fully hidden insidehttp_client.cpp.
[0.1.2] - 2026-08-28¶
Added¶
- Commands module (
../src/commands.cpp,../include/commands.hpp) (TV-17).cmd_registerandcmd_runextracted frommain.cppinto their own module in thetobornamespace, leavingmain.cppas pure CLI dispatch. First use of the header /#pragma onceconvention (D-0004). - HTTP transport module (
../src/http_client.cpp,../include/http_client.hpp) (TV-17).check_cloud_health(base_url)performs aGET <base_url>/healthvia libcurl, collecting the reply through a write callback. Hardened with a 3-second connect / 5-second total timeout, an HTTP-200 status check (a non-200 reply is a failure even when libcurl's transport reports success), and an RAII handle wrapper (CurlHandle) that always frees the curl handle on every exit path and forbids copying. Health-success policy inDECISIONS.mdD-0006. - Real
runbehavior (TV-17).cmd_runnow performs a live cloud connectivity check instead of printing a stub, exiting non-zero when the cloud is unreachable, times out, or answers non-200. - Runtime cloud URL configuration (TV-17). The cloud base URL is read from the
TOBOR_CLOUD_URLenvironment variable, with a single named dev default (http://127.0.0.1:8080, the mock cloud); endpoint paths (/health) are named constants. No cloud address is hardcoded in the transport. SeeDECISIONS.mdD-0005.
Changed¶
main.cpptidied (TV-17). File-private helpers grouped in an anonymous namespace (replacingstatic), and usage-error text factored intousage_error/unknown_command_errorso the wording lives in exactly one place.- D-0004 recorded (TV-17): headers use
#pragma oncefor include guarding, given tobor's fixed gcc/clang target set. SeeDECISIONS.mdD-0004.
[0.1.1] - 2026-08-27¶
Added¶
- Build system in
../CMakeLists.txt(TV-16). Replaces CLion's default hello-world scaffold. Baselines CMake 3.16 as the build-tool floor so tobor builds on Ubuntu 20.04 or newer. Declaresproject(tobor VERSION 0.1.1 LANGUAGES CXX), enforces C++17 strictly with no compiler extensions, adds../includeas a target include directory, turns warnings into errors in Release builds, and carries the D-0001-lstdc++fsconditional link guard for future gcc 9.4 support. docs/CHANGELOG.mditself (TV-16). Establishes the per-PR change-recording discipline that keeps this file honest.- CLI entry point in
../src/main.cpp(TV-17). First real source file. Implements-h/--helpand-v/--version(both print to stdout, exit 0); subcommand dispatch routingregisterandrunto stub handlers; and usage-error handling where no command or an unrecognized command prints a one-line message tostderrand exits2. Replaces the CLion hello-world scaffold inmain.cpp. - Compile-time version injection in
../CMakeLists.txt(TV-17).target_compile_definitionspassesPROJECT_VERSIONto the code as theTOBOR_VERSIONpreprocessor definition, sotobor --versionreads the version from the singleproject(... VERSION ...)source with no duplicated literal. - libcurl dependency wired into the build (TV-17).
find_package(CURL REQUIRED)andtarget_link_libraries(tobor PRIVATE CURL::libcurl), linking the system libcurl (OpenSSL flavour, 7.81.0) that matches the robot runtime. Rationale inDECISIONS.mdD-0003. mock-cloud/test server (TV-17). Python standard-library HTTP server standing in for the Mode A cloud on the dev laptop. Plain-HTTP endpointsGET /health,POST /register(stub JSON credential), andPOST /heartbeat. Not part of the shipped binary. HTTPS deferred to a later step.
Changed¶
- D-0002 recorded (TV-17): the build-tool baseline (CMake 3.16, from the Ubuntu 20.04 retrofit posture) is now
formally distinguished from the Ubuntu 22.04 runtime target, resolving the discrepancy flagged against D-0001.
../CMakeLists.txtstays atcmake_minimum_required(VERSION 3.16). SeeDECISIONS.mdD-0002.
Prior work (pre-changelog, not itemized)¶
TV-1 documentation and workflow tasks (TV-7 through TV-11, and any others closed before this file existed) predate this
changelog. Their outputs are visible in the git history, ../README.md, DECISIONS.md, docs/BACKLOG.md, and
docs/GIT_WORKFLOW.md. Not backfilling entries here; starting fresh from TV-16 onward.
Release history¶
(No formal tagged releases yet. The version sections above track the rolling pre-alpha increments; each bump keeps
../CMakeLists.txt and tobor --version in step. The first tagged release will be cut from these once Mode A runs on a
fielded robot.)