Skip to content

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

  • unregister decommissions a robot (TV-XX). New tobor unregister removes the stored credential so a robot can be reset, resold, or decommissioned. It asks the cloud to revoke the credential first (POST /unregister with 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 typed yes; --force skips the prompt for automation. Only the credential file is removed (/var/lib/tobor stays), so re-registering needs no root. Revoke-before-delete is also crash-safe. See DECISIONS.md D-0019.
  • remove_credential and credential_path in the store (TV-XX). remove_credential deletes the credential and returns true when the file is gone (deleted or already absent); credential_path exposes the resolved path so a command can name the file without duplicating the default. save_credential and load_credential now resolve the path through credential_path too. See DECISIONS.md D-0019.
  • send_revoke transport and mock /unregister (TV-XX). A new send_revoke posts to /unregister with the credential as a Bearer token, mirroring send_heartbeat. The mock cloud implements /unregister: it forgets the credential it issued, so a later /heartbeat with that credential returns 401. In-memory only, like the rest of the mock. See CLOUD_BACKEND.md.
  • check validates the credential (TV-XX). check (formerly run) 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 future serve command will make on a loop. See DECISIONS.md D-0016.
  • load_credential says why a read failed (TV-XX). It now returns a status (Loaded, Missing, Unreadable, or Empty) instead of just the text or nothing, so check can tell "not registered" apart from a permissions problem, a directory path, or an empty file, and report each one plainly. See DECISIONS.md D-0018.
  • Shared HTTP result classification (TV-XX). A new HttpOutcome enum and a classify() function interpret an HTTP result in one place (TLS failure, unreachable, 200, 401, other), so register and check share the logic instead of each repeating it. See DECISIONS.md D-0018.
  • Command table for the CLI (TV-XX). Commands are declared in one table (name, help line, handler), so --help and dispatch stay in sync and adding a command is one line. See DECISIONS.md D-0017.
  • mock-cloud validates heartbeats (TV-XX). /register records the credential it issues, and /heartbeat checks the Bearer credential, returning 200 for a known one and 401 otherwise, 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 native ubuntu-22.04-arm runner on every merge to main (plus a manual trigger), ending with a --version smoke 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 in DECISIONS.md D-0014.
  • Release workflow publishes the aarch64 binary (.github/workflows/release.yml) (TV-23). Pushing a version tag (v*.*.*) builds the Release aarch64 binary on ubuntu-22.04-arm, checks the tag matches the binary's version, and publishes a GitHub Release with a checksummed tobor-<version>-aarch64.tar.gz (the binary named tobor inside). 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 in DECISIONS.md D-0015.
  • Dev tooling: the mock cloud takes a --host flag (default 127.0.0.1) so it can bind 0.0.0.0 and 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 by register, check, and unregister, is factored into a single cloud_url() helper in commands.cpp (the third caller, by the rule of three), matching the existing env_or / credential_path consolidation.
  • run renamed to check (TV-XX). The connectivity command is now check; tobor run reports unknown command. A CLI-contract break, which pre-1.0 is a MINOR change, reflected in the README and CLOUD_BACKEND.md. The name serve is reserved for the future always-on online signal. See DECISIONS.md D-0016.
  • mock-cloud credential format (TV-XX). The issued credential is now tobor-cred-<serial> instead of the fixed tobor-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 is https:// (plain http:// is unchanged), driven entirely by the URL scheme. Certificate verification is always on and set explicitly (CURLOPT_SSL_VERIFYPEER and CURLOPT_SSL_VERIFYHOST); there is no switch to disable it. A new optional TOBOR_CA_BUNDLE points 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 needs ca-certificates installed). tobor presents no client certificate: this is one-way TLS, and mutual TLS remains a Mode B concern. Rationale in DECISIONS.md D-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 from CURLE_PEER_FAILED_VERIFICATION when the cloud's certificate is not trusted, or CURLE_SSL_CACERT_BADFILE when the TOBOR_CA_BUNDLE file cannot be read). is_retryable treats it as terminal, so register no longer retries a bad certificate or a mistyped CA path four times, and both register and run report 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 --key flags 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 a Content-Length header so a TLS client reads a complete body instead of failing on the connection close. The dev cert and key are generated from a documented openssl command and are git-ignored.

[0.3.0] - 2026-09-01

Added

  • Retry with backoff on register (../src/commands.cpp) (TV-20). cmd_register now wraps the one-shot register_robot in 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 private is_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 visible retrying (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 hardware serial (D-0011), so a repeat updates the same record. Rationale in DECISIONS.md D-0012.
  • env_int_or helper and two retry env knobs (../include/util.hpp) (TV-20). New header-only env_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_register uses it to expose TOBOR_REGISTER_MAX_ATTEMPTS (1 to 10, default 4) and TOBOR_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 an int.

[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). New collect_device_info() returns a DeviceInfo with the robot's hardware serial (/proc/device-tree/serial-number), machine_id (/etc/machine-id), raw os_release (/etc/os-release), and board model (/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). The serial is overridable via TOBOR_DEVICE_SERIAL for dev machines that have no device-tree. Anchor choice and the send-raw-os_release rationale in DECISIONS.md D-0011.
  • mock-cloud /register accepts the JSON payload (TV-19). The mock server parses the JSON body, requires both token and serial (missing either produces a 400), validates the token (401 on mismatch), records each device by serial in an in-memory registry, and returns the credential on success. Stands in for the cloud's device database.

Changed

  • /register request 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": ...}}, with token and serial required. The response (a JSON credential) is unchanged. cmd_register builds the payload with nlohmann/json and dump()s it before sending. See DECISIONS.md D-0011.
  • register_robot(base_url, body) (TV-19). The transport's second parameter was token; it is now a neutral body. 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). New save_credential(credential) persists the credential from a successful /register to disk, mirroring the transport seam: the module owns all file mechanics, and cmd_register just calls it and checks a bool. The write is atomic: the credential is written to a .tmp sibling which is then renamed 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 to 0600 and its directory to 0700 (owner-only), sized to the same uid the robot's containers run as, so tobor and the on-robot container can read it while other users cannot. The on-disk path defaults to /var/lib/tobor/credential and is overridable via the TOBOR_CREDENTIAL_PATH environment variable, following the D-0005 runtime-config pattern. Storage rationale (location, permissions, plaintext-vs-encryption, container access) in DECISIONS.md D-0010.
  • register saves the credential and no longer prints it (TV-19). On a 200, cmd_register now calls save_credential and reports Success! 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_or helper (TV-19). The duplicated getenv-or-default logic (a third copy appeared once the credential path was added) was factored into a single env_or(name, fallback) in ../src/commands.cpp. cmd_register and cmd_run use it for TOBOR_CLOUD_URL; the credential path resolves the same way inside credential_store.cpp, to be unified when the config file lands.

[0.1.3] - 2026-08-31

Added

  • register command (TV-19). cmd_register obtains 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 in DECISIONS.md D-0007.
  • JSON parse of the /register response (TV-19). On a 200, cmd_register parses the cloud's JSON body with the vendored nlohmann/json and extracts the credential field, replacing the temporary raw-body print. A malformed body, a missing credential key, or an empty credential is reported in plain language and exits non-zero; the parse is wrapped in try/catch, since nlohmann/json signals these by throwing. Parser choice and vendoring rationale in DECISIONS.md D-0009.
  • HTTP POST transport (TV-19). register_robot(base_url, token) sends the token to /register via a libcurl POST (CURLOPT_POSTFIELDS) and returns the result.
  • mock-cloud /register validation (TV-19). The mock server now checks the received token against a known-good value: a match returns 200 + a JSON credential, anything else returns 401 + 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 HttpResult struct (ok / status / body). check_cloud_health and register_robot now perform the request and return the raw result only; cmd_run and cmd_register do the judging (the 200 check) and all user-facing messages. Moves policy and presentation out of the transport layer. See DECISIONS.md D-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 when post_body is null, POST otherwise - leaving check_cloud_health and register_robot as thin wrappers. libcurl stays fully hidden inside http_client.cpp.

[0.1.2] - 2026-08-28

Added

  • Commands module (../src/commands.cpp, ../include/commands.hpp) (TV-17). cmd_register and cmd_run extracted from main.cpp into their own module in the tobor namespace, leaving main.cpp as pure CLI dispatch. First use of the header / #pragma once convention (D-0004).
  • HTTP transport module (../src/http_client.cpp, ../include/http_client.hpp) (TV-17). check_cloud_health(base_url) performs a GET <base_url>/health via 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 in DECISIONS.md D-0006.
  • Real run behavior (TV-17). cmd_run now 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_URL environment 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. See DECISIONS.md D-0005.

Changed

  • main.cpp tidied (TV-17). File-private helpers grouped in an anonymous namespace (replacing static), and usage-error text factored into usage_error / unknown_command_error so the wording lives in exactly one place.
  • D-0004 recorded (TV-17): headers use #pragma once for include guarding, given tobor's fixed gcc/clang target set. See DECISIONS.md D-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. Declares project(tobor VERSION 0.1.1 LANGUAGES CXX), enforces C++17 strictly with no compiler extensions, adds ../include as a target include directory, turns warnings into errors in Release builds, and carries the D-0001 -lstdc++fs conditional link guard for future gcc 9.4 support.
  • docs/CHANGELOG.md itself (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/--help and -v/ --version (both print to stdout, exit 0); subcommand dispatch routing register and run to stub handlers; and usage-error handling where no command or an unrecognized command prints a one-line message to stderr and exits 2. Replaces the CLion hello-world scaffold in main.cpp.
  • Compile-time version injection in ../CMakeLists.txt (TV-17). target_compile_definitions passes PROJECT_VERSION to the code as the TOBOR_VERSION preprocessor definition, so tobor --version reads the version from the single project(... VERSION ...) source with no duplicated literal.
  • libcurl dependency wired into the build (TV-17). find_package(CURL REQUIRED) and target_link_libraries(tobor PRIVATE CURL::libcurl), linking the system libcurl (OpenSSL flavour, 7.81.0) that matches the robot runtime. Rationale in DECISIONS.md D-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 endpoints GET /health, POST /register (stub JSON credential), and POST /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.txt stays at cmake_minimum_required(VERSION 3.16). See DECISIONS.md D-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.)