rtb-update self-update policy¶
Status: IMPLEMENTED (2026-06-26) — GTB-parity gap from the v0.22.0 audit (Phase 1). Open questions resolved 2026-06-23 (see §6).
Implementation note (2026-06-26, Wave 1): Shipped as UpdatePolicy +
baseline on ToolMetadata (rtb-app); the throttled engine
rtb_update::policy::evaluate (+ UpdateState); and a runtime pre-run
hook wired through a new rtb_app::command::BUILTIN_PRERUN_HOOKS
extension point (rtb-cli awaits it; rtb-update registers the policy
check) — chosen so rtb-cli stays decoupled from rtb-update. State lives at
<config_dir>/<tool>/update.toml. Deferred follow-ups: (1) full
Feature::Update gating — App does not carry the active Features, so
the hook gates on release_source + policy != Disabled (the Disabled
default makes it a no-op); (2) the assert_cmd e2e against a mock release
server (the release-double harness §4 flags as new work); (3) scaffolder
threading (§5); (4) the runtime config override (§2.3).
Resolutions (binding): default policy = Disabled (Q-1);
baseline-only for v0.1 — runtime update.* config override is
deferred to a later framework-config decision (Q-2); last-check state
in the consent state dir, separate file (Q-3); UpdatePolicy enum in
rtb-update, re-exported via a thin rtb-app enum to avoid a dep cycle
(Q-4); existing release_source + Feature::Update gating suffices
(Q-5).
1. Motivation¶
rtb-update (spec 2026-04-23, IMPLEMENTED) is a strong user-initiated
updater — check / run / run_from_file, RunOptions, Ed25519 +
checksum verification, atomic self-replace. It has no policy layer: a
tool built on RTB cannot say "check for updates on every run" or "block
until updated". GTB shipped this in v0.18 and it's a real parity gap.
It also closes an orphaned drift: rust-tool-base.md:410 already
documents an update.check_interval (default 24h) that was never built.
GTB's model (for reference): a three-state UpdatePolicy
(disabled/prompt/enabled) + UpdateCheckInterval baseline on the
tool, resolved at runtime (config overrides baseline), enforced pre-run
in the root command (enabled blocks every command until updated;
prompt asks; disabled warns), throttled by a persisted last-check
timestamp. Crucially this is synchronous pre-run, not a background
daemon — so it sits outside the rtb-update spec §6 non-goal of
"auto-update daemons / background checking".
2. Surface¶
2.1 UpdatePolicy (new, in rtb-app or rtb-update)¶
pub enum UpdatePolicy {
/// Never check automatically (default). User-initiated only.
Disabled,
/// Check (throttled); on a newer version, prompt the user.
Prompt,
/// Check (throttled); on a newer version, update before running.
Enabled,
}
Default = Disabled (conservative; opt-in to automation — see Q-1).
2.2 Baseline on ToolMetadata¶
Author-set defaults (mirror GTB's Tool.UpdatePolicy / UpdateCheckInterval),
both #[serde(default)] + #[builder(default)] like the existing
description / release_source fields in crates/rtb-app/src/metadata.rs:
pub update_policy: UpdatePolicy, // default Disabled
pub update_check_interval: Duration, // default 24h
2.3 Runtime override via config — DEFERRED (Q-2)¶
update.policy / update.check_interval as user-overridable config keys
are out of scope for v0.1. RTB config is the downstream tool's typed
Config<C>, not a dynamic bag, so a framework-owned update.* namespace
is a broader framework-config decision (it sets the precedent for every
future framework-level setting). v0.1 ships the author-set baseline
only; effective policy = ToolMetadata baseline (no config layer yet).
A follow-up spec will design the framework-config namespace and add the
override with precedence config update.* > baseline.
2.4 Pre-run enforcement hook¶
A check installed in Application::run / run_with_args
(crates/rtb-cli/src/application.rs:35/43), before command dispatch:
- resolve effective policy;
- if
Disabled→ no-op (skip entirely; zero overhead); - else if last-check timestamp is within
check_interval→ skip (throttle); - else run
rtb_update::check: Enabled+ newer available → update (atomic self-replace) then continue, or abort with a clear diagnostic if update fails;Prompt+ newer available → ask (TTY) / print notice (non-interactive);- record the last-check timestamp regardless.
Skipped automatically when Feature::Update is runtime-disabled or
release_source is None (the existing update gating).
3. State¶
A persisted last-check timestamp (and last-seen-version) to drive
throttling. Store under the tool's state dir (XDG state / the same place
consent.toml lives — see Q-3). Never blocks if the state file is
unreadable (fail-open: just re-check).
4. Testing (TDD)¶
- Unit: policy resolution (config > baseline > default); throttle
decision given a timestamp + interval;
Disabledshort-circuits. - E2E (
assert_cmd): a scaffolded tool withEnabled/Prompt/Disabledbaselines + a stubReleaseProvider→ assert the pre-run behaviour (block / prompt-notice / silent) and that the throttle suppresses a second check within the interval. Use the injectable release double (GTB added areleasetestdouble in v0.18 — RTB needs the equivalent).
5. Scaffolder threading (follow-up, not v0.1)¶
GTB scaffolds the policy into generated tools (skeleton_root.go +
manifest). For RTB, the generated main.rs Application::builder() would
set .update_policy(...). Defer to a scaffolder follow-up once the
runtime policy lands; note it in the v0.7 scaffolder scope.
6. Resolutions (resolved 2026-06-23)¶
- [Q-1] Default polarity →
Disabled. Surprise-free; tools opt in toPrompt/Enabledexplicitly. Zero pre-run overhead / no unsolicited network calls by default. - [Q-2] Framework config namespace → DEFERRED; baseline-only for v0.1.
Ship the
ToolMetadatabaseline + pre-run hook now; the framework-ownedupdate.*typed-config namespace is a separate, broader decision (see §2.3). No runtime config override in v0.1. - [Q-3] Last-check state location. Reuse the
consent.tomlstate dir (cli-ops) vs a dedicatedupdate.toml/ state file. Recommend the same state dir, separate file. - [Q-4] Crate placement.
UpdatePolicyenum + resolution inrtb-update(cohesive) vsrtb-app(soToolMetadatadoesn't depend onrtb-update). Recommend the enum inrtb-update, re-exported; or a thin enum inrtb-appto avoid the dep cycle. Confirm the dep direction. - [Q-5] Interaction with
release_sourcerequirement. Already gated: policy is inert withoutFeature::Update+ arelease_source. Confirm no new validation needed.
7. Out of scope¶
- Background/daemon update checking (still a non-goal — this is synchronous pre-run only).
- Scaffolder threading (§5, deferred follow-up).
- Changing the existing user-initiated
check/runAPI.