Skip to content

The consent file

The consent module persists a user's decision about collection so the answer survives between runs. It is a small, deliberately boring format.

On-disk format

version = 1
state = "disabled"
decided_at = "2026-08-02T20:31:52.918653411Z"
Key Type Required Meaning
version integer yes Schema version. Must be exactly 1.
state "enabled" | "disabled" | "unset" yes The recorded decision.
decided_at string no RFC 3339 UTC timestamp of the decision. Omitted when absent.

decided_at is written with the sub-second precision the clock reports, as above — not truncated to whole seconds. It is stored as a plain string rather than a parsed timestamp so that a future schema version can carry a richer format without breaking deserialisation of version 1.

Where the file lives

This crate does not choose the path. Every function takes a &Path from the caller. The convention across the toolkit is <config_dir>/<tool>/consent.toml, and the CLI runtime (rtb-cli) resolves <config_dir> from directories::ProjectDirs and owns the telemetry status / enable / disable / reset commands that write it. A tool not built on rtb-cli picks its own path and gets to keep both halves.

ConsentState and how it maps to a policy

ConsentState CollectionPolicy
Enabled Enabled
Disabled Disabled
Unset (the Default) Disabled

Unset and Disabled both mean "do not collect". They are distinct because a tool usually wants to prompt someone who has never been asked and never prompt someone who has said no.

The conversion is a From impl, so CollectionPolicy::from(state) or state.into() both work.

Constructors

Constructor State decided_at
Consent::unset() Unset None
Consent::enabled_now() Enabled current UTC time
Consent::disabled_now() Disabled current UTC time

Consent::SCHEMA_VERSION is the constant 1, and every constructor stamps it.

Reading, writing and deleting

consent::read(path)  -> Result<Option<Consent>, TelemetryError>
consent::write(path, &consent) -> Result<(), TelemetryError>
consent::reset(path) -> Result<(), TelemetryError>

read returns Ok(None) when the file does not exist — a missing file is not an error, and callers treat it as Unset. It returns TelemetryError::Serde for malformed TOML and for a version other than 1:

serialisation error: consent: unsupported schema version 99 (expected 1)

That is a hard refusal, not a downgrade: a file written by a future version of the format will stop the tool reading consent rather than being guessed at. Any other filesystem failure — a permissions problem, say — comes back as TelemetryError::Io.

write creates parent directories on demand and serialises with toml::to_string_pretty. It is not atomic: it writes in place, so a process killed mid-write can leave a truncated file, which the next read will reject as malformed. If that matters, write to a temporary file yourself and rename it over the top.

reset deletes the file and returns Ok(()) if it was already absent, so telemetry reset is idempotent. It deletes the decision, not any collected data — see What this does not do.

let policy = match consent::read(&path)? {
    Some(record) => CollectionPolicy::from(record.state),
    None => CollectionPolicy::Disabled,
};

That is the whole integration. Nothing in the crate reads the file for you, and nothing watches it for changes: a context built from a policy keeps that policy until it is rebuilt.