Skip to content

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; Disabled short-circuits.
  • E2E (assert_cmd): a scaffolded tool with Enabled/Prompt/Disabled baselines + a stub ReleaseProvider → 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 a releasetest double 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 to Prompt/Enabled explicitly. Zero pre-run overhead / no unsolicited network calls by default.
  • [Q-2] Framework config namespace → DEFERRED; baseline-only for v0.1. Ship the ToolMetadata baseline + pre-run hook now; the framework-owned update.* 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.toml state dir (cli-ops) vs a dedicated update.toml / state file. Recommend the same state dir, separate file.
  • [Q-4] Crate placement. UpdatePolicy enum + resolution in rtb-update (cohesive) vs rtb-app (so ToolMetadata doesn't depend on rtb-update). Recommend the enum in rtb-update, re-exported; or a thin enum in rtb-app to avoid the dep cycle. Confirm the dep direction.
  • [Q-5] Interaction with release_source requirement. Already gated: policy is inert without Feature::Update + a release_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/run API.