Rust Tool Base — Requirements & Specification¶
Status: Draft v0.1. Normative for the 0.x line.
Audience: contributors to rust-tool-base; downstream CLI authors
evaluating adoption.
0. Scope & non-goals¶
0.1 Goal¶
Provide a batteries-included, opinionated Rust application framework for building production-grade CLI tools and adjacent applications (daemons, agents, MCP servers), with the lifecycle (version, update, docs, init, MCP, telemetry) wired by default.
0.2 Non-goals¶
- Not a port of Go Tool Base. RTB targets the same outcomes, but uses idiomatic Rust mechanisms throughout. Go paradigms that are inappropriate in Rust are replaced, not transliterated.
- Not a web framework. Integrates
axumfor tools that need aserve-style subcommand; does not replaceaxum. - Not a TUI library. Uses
ratatui,inquire,termimad. - Not a dependency-injection container. The
Appcontext is a plain Rust struct passed by cheap clone. - No custom async runtime. Picks
tokio. - No custom logging abstraction. Uses
tracing.
0.3 Paradigm swaps vs. GTB (normative)¶
| GTB (Go) | RTB (Rust) | Rationale |
|---|---|---|
Props struct with any-typed config |
Generic App<C: AppConfig> + strongly-typed serde config |
Types over strings; compile-time checking |
Functional options (func(opts *X)) |
Typestate builders via bon |
Required fields enforced at compile time |
context.Context threaded through every call |
tokio_util::sync::CancellationToken + async fn propagation with ? |
Structured concurrency; no value-bag |
Package-level init() self-registration |
#[linkme::distributed_slice] |
No life-before-main; link-time safety |
Containable interface for config |
figment::Figment + caller's serde::Deserialize type |
Typed access, not dynamic accessors |
ErrorHandler.Check() at Execute() |
main() -> miette::Result<()> + installed miette hook |
Errors as values, reported at the edge |
afero.Fs everywhere |
vfs only for overlay; std::fs/tokio::fs otherwise |
Pay overlay cost only where needed |
interface{} slog key-value pairs |
tracing structured fields |
Compile-time macro expansion |
cmd.go + main.go split per subcommand |
Rust module per command, file count by size | Rust modules already scope things |
chan + goroutines |
tokio::sync::mpsc/broadcast/watch + tasks |
Native async primitives |
1. Glossary¶
- Application — the downstream binary built on RTB.
- App context (
App) — cheap-clonable struct carrying framework services (config, assets, logging, shutdown token, tool metadata). - Command — a type implementing
rtb_cli::Commandthat is registered with anApplication. - Feature — a runtime switch (
rtb_app::Feature) for built-in commands and subsystems. Orthogonal to Cargo features. - Cargo feature — a compile-time on/off for a slice of RTB, exposed by
the
rtbumbrella crate (cli,update,docs,mcp,ai,credentials,tui,telemetry,vcs,full).
2. Workspace & crate topology¶
2.1 Workspace root¶
Single Cargo workspace at Cargo.toml. resolver = "2". Pinned toolchain
via rust-toolchain.toml (stable). MSRV is whatever that stable channel
supports at the time of a release; documented in each crate's
rust-version field.
2.2 Crate list¶
| Crate | Role | Hard deps (public API) |
|---|---|---|
rtb-error |
Error enum, Result, miette glue |
miette, thiserror |
rtb-app |
App, ToolMetadata, VersionInfo, Features, registration slices |
rtb-error, rtb-config, rtb-assets |
rtb-config |
Layered typed config; hot-reload | figment, notify, arc-swap, serde |
rtb-assets |
rust-embed + vfs overlay |
rust-embed, vfs, format deps |
rtb-cli |
Application builder, clap integration, built-in commands |
clap, tracing, tokio, miette, rtb-app |
rtb-update |
Self-update (archive + signature + atomic swap) | self_update, self-replace, ed25519-dalek, sha2 |
rtb-vcs |
Git + GitHub + GitLab abstractions | gix, octocrab, gitlab, secrecy |
rtb-ai |
Multi-provider AI client; structured output | genai, async-openai, schemars, jsonschema |
rtb-mcp |
MCP server that exports registered commands | rmcp, rtb-cli |
rtb-docs |
ratatui-based docs browser; streaming AI Q&A |
ratatui, termimad, rtb-assets, rtb-ai (feature-gated) |
rtb-tui |
Wizard, tables, spinners |
inquire, ratatui, tabled |
rtb-credentials |
CredentialStore trait + KeyringStore impl |
keyring, secrecy, zeroize |
rtb-telemetry |
Opt-in telemetry sinks + OTLP layer | machine-uid, sha2, opentelemetry_*, tracing-opentelemetry |
rtb |
Umbrella — re-exports everything behind Cargo features | all of the above |
rtb-cli-bin |
The rtb scaffolder/regenerator binary |
clap, minijinja, inquire |
2.3 Directory layout¶
Mirrors the GTB layout conceptually but uses Rust conventions (no
pkg//cmd//internal/ split; Rust's module system handles that).
rust-tool-base/
├── Cargo.toml
├── crates/ # library + binary crates
├── examples/ # reference tool(s) built on rtb
├── docs/ # mkdocs-compatible user docs
├── assets/init/config.yaml # default embedded config
├── .github/workflows/ # ci, release
├── deny.toml
├── rustfmt.toml
├── rust-toolchain.toml
├── justfile
├── LICENSE
└── SECURITY.md
2.4 Feature flags on the rtb umbrella¶
| Feature | Default? | Enables |
|---|---|---|
cli |
yes | rtb-cli, clap |
update |
yes | rtb-update, rtb-vcs release providers |
docs |
yes | rtb-docs |
mcp |
yes | rtb-mcp |
credentials |
yes | rtb-credentials |
ai |
no | rtb-ai |
tui |
no (brought in transitively by docs) |
rtb-tui |
telemetry |
no | rtb-telemetry |
vcs |
no (brought in transitively by update) |
rtb-vcs explicitly |
full |
no | all of the above |
3. Core application context (rtb-app)¶
3.1 App struct¶
#[derive(Clone)]
pub struct App {
pub metadata: Arc<ToolMetadata>,
pub version: Arc<VersionInfo>,
pub config: Arc<Config>, // typed; see §4
pub assets: Arc<Assets>, // overlay FS; see §5
pub shutdown: CancellationToken, // tokio_util; see §11
}
Normative:
- All fields are
Arc-wrapped soAppis cheap to clone. Command handlers takeAppby value. Appis notDefault. It is constructed only via theApplicationbuilder inrtb-cli.- There is no
Arc<dyn …>inAppby design. Runtime polymorphism (e.g. adyn CredentialStore) lives inside the relevant subsystem, not on the context.
3.2 Config generic parameter¶
In the public API, App is actually App<C = ()> where C: AppConfig.
AppConfig is a trait with a blanket impl for any T: DeserializeOwned +
Send + Sync + 'static. This keeps App strongly typed to the downstream
tool's config struct without sacrificing ergonomics.
pub trait AppConfig: DeserializeOwned + Send + Sync + 'static {}
impl<T: DeserializeOwned + Send + Sync + 'static> AppConfig for T {}
pub struct App<C: AppConfig = ()> {
// …
pub config: Arc<Config<C>>,
}
3.3 ToolMetadata¶
Built with bon::Builder. See crates/rtb-app/src/metadata.rs. All fields
except name and summary are optional. release_source is required iff
Feature::Update is enabled at runtime (checked in Application::build).
3.4 Features¶
A HashSet<Feature> wrapped in a newtype with a FeaturesBuilder
exposing .enable(f)/.disable(f). The set is owned by the Application
builder, not placed on App itself — handlers do not need to query
features.
3.5 Command registration¶
RTB provides two orthogonal registration mechanisms:
-
Explicit (preferred for downstream tools):
Application::builder().command::<MyCommand>()— an inherent trait- objectBox<dyn Command>is added. -
Distributed-slice (for RTB's built-ins and for plugin crates):
use linkme::distributed_slice;
#[distributed_slice]
pub static BUILTIN_COMMANDS: [fn() -> Box<dyn Command>];
#[distributed_slice(BUILTIN_COMMANDS)]
fn register_version() -> Box<dyn Command> { Box::new(VersionCmd) }
Application::build walks BUILTIN_COMMANDS and filters by Features. No
life-before-main, no mutex-guarded registry.
3.6 Command trait¶
#[async_trait::async_trait]
pub trait Command: Send + Sync + 'static {
/// Uniquely identifies the subcommand path, e.g. "deploy", "config get".
fn spec(&self) -> CommandSpec;
/// Execute with the app context and the already-parsed clap matches.
async fn run(&self, app: App<Self::Config>, matches: &clap::ArgMatches) -> miette::Result<()>;
type Config: AppConfig = ();
}
A CommandSpec describes the static command surface: name, about, aliases,
and the Feature it gates on. MCP-tool exposure is opt-in via the
Command::mcp_exposed / Command::mcp_input_schema default trait methods
(see rtb-mcp). Downstream authors will
eventually derive Command via a #[rtb::command] attribute macro that
generates spec() and the MCP hooks from struct fields — see §12.
4. Configuration (rtb-config)¶
4.1 Design statement¶
Configuration is typed and layered. Dynamic Containable.GetString-
style access is explicitly rejected.
4.2 Layering order (last-wins)¶
- Embedded default (
assets/init/config.yaml, or caller-supplied&str). - User/system file(s) (
~/.config/<tool>/config.{yaml,toml,json}viadirectories::ProjectDirs; plus any explicit--config <path>). - Environment variables, prefix
"<TOOL>_"(configurable). - Command-line flags.
4.3 Builder¶
let cfg: Config<MyConfig> = Config::<MyConfig>::builder()
.embedded_default(include_str!("../assets/init/config.yaml"))
.user_files_yaml(ProjectDirs::from("dev", "me", "mytool"))
.env_prefixed("MYTOOL_")
.cli_overrides(&matches) // serde_json::Value from clap
.watch(true)
.build()?;
4.4 Hot reload¶
notify-debouncer-fullpolls the registered user files.- On change, the new
figment::Figmentis re-extracted intoC. - The parsed value is swapped atomically into
arc_swap::ArcSwap<C>. - Subscribers call
cfg.subscribe() -> watch::Receiver<Arc<C>>(backed bytokio::sync::watch) to react. - No observer pattern in the GTB sense. The
watchchannel is the idiomatic Rust alternative — pull-based,Clone-friendly, survives subscriber death gracefully.
4.5 Profiles¶
For layered profiles (think "dev", "prod") use Figment::select(profile)
from figment. RTB exposes this as Config::with_profile(&str) on the
builder; no runtime Sub() accessor.
4.6 Schema + validation¶
- Downstream tools may derive
schemars::JsonSchemaon their config. Config::schema()returns the JSON Schema (used byconfig schemasubcommand and by MCP introspection).- Validation is compile-time via
serdedeserialisation. For richer invariants, implementTryFrom<Raw>for yourConfig.
5. Assets & overlay FS (rtb-assets)¶
5.1 Layering¶
#[derive(rust_embed::RustEmbed)]
#[folder = "assets/"]
pub struct EmbeddedAssets;
let embedded: VfsPath = EmbeddedFS::<EmbeddedAssets>::new().into();
let userdir: VfsPath = PhysicalFS::new(
ProjectDirs::from("dev", "me", "mytool").unwrap().data_dir(),
).into();
let overlay: VfsPath = OverlayFS::new(&[userdir, embedded]).into();
5.2 Structured-data merging¶
For each of .yaml/.json/.toml, RTB merges across layers using
json-patch::merge on serde_json::Value (the two non-JSON formats round-
trip via serde_json). Last-registered (top) layer wins at the leaf; maps
merge recursively.
5.3 Binary assets¶
For non-structured blobs, last-registered-wins shadowing only — no concatenation.
5.4 CSV¶
CSVs are appended with header-deduplication across layers (mirroring GTB's behaviour).
5.5 Dev vs. release¶
rust-embed compiles in bytes in release mode. In debug mode the same API
reads from disk, so authors see live edits without rebuilding. RTB does not
override this behaviour.
6. Logging & diagnostics (tracing + miette)¶
6.1 Logging¶
- The
Applicationinstalls atracing_subscriber::registry()with these layers (gated by config/env): fmt::layer().with_target(false).compact()for pretty terminal output when stderr is a TTY.fmt::layer().json()whenlog.format = jsonor stderr is not a TTY.tracing_opentelemetry::layer().with_tracer(…)when thetelemetryCargo feature is on and OTLP is configured.- Level is controlled by
RUST_LOG(standard) orlog.level.
RTB does not define its own Logger trait. Callers use the tracing
macros (info!, warn!, error!, span!) directly.
6.2 Diagnostics¶
- Every crate derives
thiserror::Error + miette::Diagnostic. rtb-cli::Application::run()installs:miette::set_hook(Box::new(|_| Box::new(GraphicalReportHandler::new())))miette::set_panic_hook()- Errors carry
#[diagnostic(code(...), help(...), url(...))]. The custom hook additionally consultsToolMetadata::help(the GTB "support channel" concept) and appends a contact line.
7. Error type (rtb-error)¶
Errorenum with#[non_exhaustive].pub type Result<T, E = Error> = std::result::Result<T, E>;Other(Box<dyn Diagnostic + Send + Sync + 'static>)variant preserves downstream typed diagnostics.
RTB does not export a WithHint() helper — miette::Diagnostic's help
attribute fills that role.
8. Built-in commands¶
Status (as of v0.3):
version,doctor,init,config show,update,docs, andmcpall ship as real implementations.updateis registered byrtb-update(v0.2),docsbyrtb-docs(v0.2), andmcpbyrtb-mcp(v0.3).changelogis not yet wired.--output jsonis deferred. See §16 for the roadmap.
All built-ins are registered into BUILTIN_COMMANDS behind Cargo features
and runtime-filtered by Features. Each supports --output text|json
(v0.2).
8.1 init¶
- Prompts via
inquire::Wizard(RTB'srtb-tui::Wizard). - Writes the merged default config to
~/.config/<tool>/config.yaml. - Invokes registered
Initialisers (trait object inBUILTIN_INITIALISERSdistributed slice, mirroringBUILTIN_COMMANDS). - Skippable per-initialiser via
--skip=<name>.
8.2 version¶
- Prints
version,commit,date,rustc, and the detected target triple. - With
--check, issues aReleaseProvider::latest()call.
8.3 update¶
- Uses
self_updateto find the matching release asset. - Downloads, verifies SHA-256, then Ed25519 signature against the pinned
public key in
ToolMetadata. - Extracts via
tar + flate2(orzip). - Swaps with
self-replace. - Supports
--file <archive>for offline installs. - Throttled: writes a timestamp to
ProjectDirs::cache_dir()and won't re-check withinupdate.check_intervalhours (default 24).
8.4 docs¶
- TUI browser (
ratatui) over the overlay-merged/docstree. - Two-pane layout: tree sidebar +
termimad-rendered markdown. /for fuzzy search.- With the
aiCargo feature:docs ask "<question>"runs a RAG loop over the merged tree viartb-ai.
8.5 mcp¶
- Boots an
rmcpserver over stdio (default).Transport::SseandTransport::Httpvariants are present on the API; full streamable-HTTP wiring lands in v0.3.x. - Subcommands:
mcp serve [--transport stdio|sse|http] [--bind ADDR]andmcp list(prints every exposed tool's name + description + JSON schema as one JSON object per line). - Per-command opt-in via the
Command::mcp_exposed(defaultfalse) andCommand::mcp_input_schema(defaultNone) trait methods onrtb_app::command::Command. - See
rtb-mcpv0.1 spec for the full contract.
8.6 doctor¶
- Runs
HealthChecktrait objects registered inBUILTIN_HEALTH_CHECKS. - Built-ins: config validity, keychain reachability, release-provider reachability, filesystem permissions.
- Emits a report table (
tabled) + JSON envelope.
8.7 config¶
config get <jsonpath>,config set <jsonpath> <value>,config schema,config validate.- Mutations write to the highest-priority writable user file.
8.8 changelog¶
- Parses
CHANGELOG.md(Keep-a-Changelog / conventional-commit format). - Prints the entries for the current version, or for a
--sincerange.
9. VCS & release providers (rtb-vcs)¶
Status: ✅ shipped. Release-provider slice shipped at 0.2.0 (release backends +
ReleaseProvidertrait, consumed byrtb-update). Git-operations slice shipped at the v0.5 milestone —Repotype with init / open / clone / walk / diff / blame / status / commit / fetch / checkout / push. See the v0.5 scope addendum for the per-commit history and design resolutions; seedocs/components/rtb-vcs.mdfor the user-facing reference.
9.1 Release provider trait¶
#[async_trait::async_trait]
pub trait ReleaseProvider: Send + Sync {
async fn latest_release(&self) -> Result<Release, ProviderError>;
async fn release_by_tag(&self, tag: &str) -> Result<Release, ProviderError>;
async fn list_releases(&self, limit: usize) -> Result<Vec<Release>, ProviderError>;
async fn download_asset(&self, asset: &ReleaseAsset)
-> Result<(Box<dyn AsyncRead + Send + Unpin>, u64), ProviderError>;
}
Selected at runtime from ToolMetadata::release_source. Implementations
live behind Arc<dyn ReleaseProvider>; the downstream tool never imports
octocrab or gitlab directly. Built-in backends register at link time
via linkme::distributed_slice on RELEASE_PROVIDERS — rtb_vcs::lookup
and rtb_vcs::registered_types are the discovery entry points.
9.2 Git ops — Repo¶
The Repo type is the async git wrapper. Public method shape:
impl Repo {
pub async fn init(path, InitOptions) -> Result<Self, RepoError>;
pub async fn open(path) -> Result<Self, RepoError>;
pub async fn clone(url, dst, CloneOptions) -> Result<Self, RepoError>;
pub fn walk(&self, revspec) -> Result<CommitWalk, RepoError>; // async stream
pub async fn diff(&self, a, b) -> Result<Diff, RepoError>;
pub async fn blame(&self, path, revspec) -> Result<Blame, RepoError>;
pub async fn status(&self) -> Result<RepoStatus, RepoError>;
pub async fn commit(&self, paths, message) -> Result<String /* OID */, RepoError>;
pub async fn fetch(&self, remote, FetchOptions) -> Result<(), RepoError>;
pub async fn checkout(&self, revspec, CheckoutOptions) -> Result<(), RepoError>;
pub async fn push(&self, remote, refspec, PushOptions) -> Result<(), RepoError>;
}
Backend choice (per A8 wrap-not-leak):
gixis the read-path backend (open,walk,diff,status, anonymousclone). Gated on thegitCargo feature (default-on per A7).gix-blamepowersblame, with per-lineBlameLineoutput rather than gix's hunk-level entries.- Write paths shell out to
git.commit,fetch,checkout, authenticatedclone,pushinvoke the hostgitCLI undertokio::task::spawn_blocking. Rationale: gix 0.72 has no high-level helpers for staging + commit + auth and rebuilding them would be 50+ lines of fiddly plumbing per op. Migration to pure-gix is internal — public API stable. - Errors are wrapped, not leaked.
RepoErrorcarries semantic variants with stringifiedcause; gix / git2 / git stderr never reach the public API. - The originally-planned
git2-fallbackCargo feature was obsoleted (v0.5 A4) — consistent shell-out for all write ops removed the need for libgit2.
9.3 Token resolution¶
Auth-bearing Repo methods (clone, fetch, push) take an
optional CredentialRef on their options struct
(CloneOptions::with_credential, etc.). When set, rtb-vcs
resolves the credential via
rtb_credentials::Resolver::with_platform_default() — the same
env → keychain → literal → fallback-env precedence chain as
everywhere else in the framework.
The resolved SecretString is passed to the git subprocess via:
RTB_VCS_GIT_TOKEN=<secret>in the subprocess env.-c credential.helper='!f() { echo username=x-access-token; echo password=$RTB_VCS_GIT_TOKEN; }; f'in argv (helper snippet contains no secret).GIT_TERMINAL_PROMPT=0to fail fast on auth errors.
Username is hardcoded as x-access-token — GitHub PAT convention,
accepted by GitLab / Gitea / Codeberg. Tools needing other
usernames can wrap Repo::clone / fetch / push themselves.
Unresolvable credentials surface as RepoError::Auth before any
network IO; resolved-but-rejected credentials surface as
CloneFailed / FetchFailed / PushFailed with git's stderr in
cause.
10. AI client (rtb-ai)¶
Status: ⏳ deferred to v0.3.
rtb-aiexists as a stub crate. The spec below is the design target; nothing in this section is yet implemented. Credentials for AI providers will flow throughrtb-credentials'sResolver— that part is v0.1 shipped. See §16 for the full roadmap.
10.1 Providers¶
Provider set matches GTB: Claude (Anthropic), ClaudeLocal, OpenAI,
OpenAICompatible, Gemini. Added: Ollama (via genai).
10.2 API surface¶
pub struct AiClient { /* … */ }
impl AiClient {
pub async fn chat(&self, prompt: &str) -> Result<String>;
pub async fn ask<T: JsonSchema + DeserializeOwned>(&self, prompt: &str) -> Result<T>;
pub async fn chat_stream(&self, prompt: &str) -> Result<impl Stream<Item = Result<String>>>;
pub async fn react<Ts: ToolSet>(&self, tools: Ts, prompt: &str) -> Result<ReactOutcome>;
}
ask::<T>inserts a JSON Schema (generated viaschemars) in the request and validates the response withjsonschemabefore deserialising. This is RTB's "structured output" guarantee.react::<Ts>orchestrates a bounded ReAct loop (config.ai.max_steps). Parallel tool execution viatokio::join!-style fan-out, capped byconfig.ai.max_parallel_tools.
10.3 Anthropic-specific features¶
For prompt caching, extended thinking, citations, managed agents, and the
Files API, the claude backend drops down to direct reqwest calls
against the Anthropic Messages API. This is isolated behind the same
AiClient façade, so callers remain portable across providers.
11. Concurrency & lifecycle¶
11.1 Runtime¶
tokio multi-thread flavour. rtb-cli exposes a run() that enters a
Runtime if one is not already present; prefer the #[tokio::main]
pattern for downstream tools.
11.2 Shutdown¶
App::shutdownis aCancellationToken.rtb-cliwirestokio::signal::ctrl_c()+ (on Unix)SIGTERMviasignal-hooktoshutdown.cancel().- Subsystems derive child tokens (
shutdown.child_token()). A parent cancellation cascades. - Long-running work uses
tokio::select!to race againstshutdown.cancelled(). No "controller" service-manager type is provided; theJoinSetinstd/tokio is sufficient.
11.3 No Controller / service manager¶
GTB's controls package supervises long-running services (HTTP, workers).
In Rust this is the natural fit for tokio::task::JoinSet plus the
tokio_graceful_shutdown crate when tiered shutdown is desired. RTB
exposes a thin helper rtb_cli::services::run_services(Vec<BoxedService>,
CancellationToken) but does not define a new abstraction.
12. Command authoring experience¶
Status (as of v0.1): the
#[rtb::command]attribute macro is deferred to v0.2+ (pending a real usage pattern to crystallise around). v0.1 command authoring is hand-writtenimpl Commandwith inlineCommandSpec+ a#[distributed_slice(BUILTIN_COMMANDS)]factory. Error ergonomics (§12.2) ARE shipped. Seeexamples/minimalfor the v0.1 pattern.
12.1 Macro (deferred)¶
use rtb::prelude::*;
#[rtb::command]
#[command(about = "Deploy the thing")]
pub struct Deploy {
/// Environment name.
#[arg(long, short)]
env: String,
/// Dry run.
#[arg(long)]
dry_run: bool,
}
#[async_trait::async_trait]
impl rtb::Command for Deploy {
type Config = crate::Config;
async fn run(&self, app: App<crate::Config>, _: &ArgMatches) -> miette::Result<()> {
info!(env = %self.env, "deploying");
Ok(())
}
}
#[rtb::command]expands into:clap::Argsderive on the struct.CommandSpecconstruction.- A
fn __rtb_register()inserted intoBUILTIN_COMMANDSvialinkme. #[rtb::command(mcp)]additionally derivesschemars::JsonSchema, overridesCommand::mcp_exposedtotrue, and emits anmcp_input_schemabody that returns the derived JSON Schema.
Status (as of v0.3): the
#[rtb::command]attribute macro is not yet shipped — author commands by hand-implementingCommandfor now. Until the macro lands, MCP-exposed commands implementmcp_exposed/mcp_input_schemadirectly:
12.2 Error ergonomics¶
Return miette::Result<()>; use ? freely. For ad-hoc hints:
return Err(miette::miette!(
help = "run `mytool init` first",
code = "mytool::no_config",
"no config file found in {}",
path.display()
));
13. Security requirements (normative)¶
#![forbid(unsafe_code)]in every workspace crate. Enforced viaworkspace.lints.rust.- Secrets: API tokens, keychain lookups, and Git credentials must
transit the codebase as
secrecy::SecretString.Debugmust render[REDACTED]. - TLS:
reqwest+axum-servermust be built with therustls-tlsfeature.native-tlsis explicitly disallowed. - Update verification: SHA-256 digest and Ed25519 signature are
required before
self-replaceruns. Tools may ship their own public key viaToolMetadata::update_verification_key. - Telemetry: opt-in at both author and user levels. Machine ID is
salted-SHA-256 of
machine-uid; never the raw ID. cargo-denyruns in CI with the policy indeny.toml.- Regex compiled from user input uses
RegexBuilder::size_limit(1 MiB)+dfa_size_limit(8 MiB).
14. CI / release engineering¶
ci.yaml: rustfmt, clippy (-D warnings),cargo nexteston {linux, macOS, windows} x stable,cargo-deny,cargo docwith-D warnings.release.yaml: tag-triggeredcargo-dist(renameddist) producing signed multi-platform artefacts; publishes to crates.io in dependency order.- Version bumping via
cargo-release. CHANGELOG.mdauthored in Keep-a-Changelog format and parsed by thechangelogsubcommand.
15. Acceptance criteria — 0.1 release¶
Status: ✅ 0.1.0 shipped 2026-04-22. All 7 criteria below are closed (1–7). Criterion 8 (
rtb-cli-binscaffolder) deferred to v0.6 per the roadmap revision. SeeCHANGELOG.mdfor the per-crate detail.
Minimum shippable scope:
- ✅ Workspace compiles clean (
just ci). - ✅
rtb-appexposesApp,ToolMetadata,Features,Commandtrait,BUILTIN_COMMANDS. (Application::builderlives inrtb-clirather thanrtb-app.) - ✅
rtb-configsupports env + user file + embedded default layering, typed viaserde::Deserialize. Explicitreload()ships; reactivesubscribe()+ notify-driven auto-reload deferred to v0.2. - ✅
rtb-assetsexposes the overlay FS with YAML/JSON deep merging. TOML deep-merge deferred to v0.2. - ✅
rtb-errorexposes theErrorenum andmiettehook helpers. - ✅
rtb-cliwires the above and shipsversion,doctor,init,config show.update,docs,mcpship asFeatureDisabledstubs that get replaced by their respective crates' v0.2+ registrations (see §8). - ✅
examples/minimalruns end-to-end on Linux/macOS/Windows and is covered by anassert_cmdsmoke test inexamples/minimal/tests/smoke.rs. - ⏳
rtb-cli-binscaffolds a new project (rtb new). Deferred to v0.6; v0.1 ships the binary as a stub to reserve thertbcommand name.
16. Roadmap¶
Shipped¶
- 0.1.0 (2026-04-22) —
rtb-error,rtb-app,rtb-config,rtb-assets,rtb-cli,rtb-credentials,rtb-telemetry. 151 acceptance criteria green. SeeCHANGELOG.mdanddocs/development/specs/2026-04-22-*.mdfor per-crate detail. - 0.2.0 (2026-05-01) —
rtb-redact,rtb-vcsv0.1 (release slice),rtb-updatev0.1,rtb-docsv0.1,rtb-confighot-reload, OTLP + HTTP JSON sinks inrtb-telemetry.update,docs, andmcpstubs removed fromrtb-cli. See2026-04-23-v0.2-scope.mdfor scope rationale. - 0.3.0 (2026-05-09) — the "operator surface" release. Bundles framework-spec milestones 0.3 and 0.4 into a single shipped workspace version:
- Spec milestone 0.3 —
rtb-aiv0.1 (genai + Anthropic-direct for caching),rtb-mcpv0.1 (rmcpSDK; commands self-register as MCP tools viaCommand::mcp_exposed). Structured output viaschemars+jsonschema. See2026-05-01-v0.3-scope.md. - Spec milestone 0.4 —
rtb-tuiv0.1 (Wizard, table/json render helpers, TTY-awareSpinner);rtb-clicredentials/telemetry/ extendedconfigsubtrees; global--output text|jsonflag.Appgainscredentials_providerApp::credentials();rtb-credentialsaddsCredentialBearing+Resolver::probe;rtb-configadds themutablefeature withConfig::schema/Config::write;rtb-telemetryadds the persistedconsentmodule. See2026-05-06-v0.4-scope.md.
- Schemars 0.8 → 1.x and tantivy 0.22 → 0.26 upstream bumps landed on develop after release; they ship in the next workspace version.
- 0.4.1 (2026-05-09) — App
typed-config integration. Appgains type-erased typed-config storage withApp::typed_config<C>/App::config_as<C>recovery,App::config_schema/config_valuerendering, andApplication::builder().config<C>(...).rtb-cli'sconfig show / get / set / schema / validatelight up the schema-aware paths automatically when the host tool opts in. See2026-05-09-v0.4.1-scope.md. - 0.5.0 —
rtb-vcsv0.2 (git-operations slice). TheRepotype withinit/open/clone/walk/diff/blame/status/commit/fetch/checkout/push. Read paths viagix+gix-blame; write paths via shell-out togit(consistent backend choice obsoleted the originally-plannedgit2-fallbackopt-in — see A4). Auth viartb-credentials::Resolver - inline credential.helper. See
2026-05-11-v0.5-scope.md.
Pending¶
- 0.6 —
rtb-cli-binscaffolder withrtb new,rtb generate,rtb regenerate. - 1.0 — API freeze, semver commitment, full docs site.
Appendix A — Crate selection rationale¶
See docs/about/ecosystem-survey.md
for a condensed table with alternatives considered.
Appendix B — Explicit anti-patterns¶
The following Go-isms are rejected in RTB. Contributors proposing them must justify against the alternative listed.
| Anti-pattern | Preferred Rust alternative |
|---|---|
map[string]any config |
serde::Deserialize struct + figment::Figment |
Functional options (func(*Options)) |
bon::Builder typestate |
Package init() for registration |
linkme::distributed_slice |
context.Context threading |
CancellationToken + async ? |
ErrorHandler.Check() funnel |
main() -> miette::Result<()> with installed hook |
interface{} DI container |
Strongly-typed App<C> struct |
any slog field values |
tracing structured fields |
chan struct{} for cancellation |
CancellationToken::cancelled() |
goroutine pool with WaitGroup |
JoinSet with cancellation |
Sub(key) dynamic config |
figment::select(profile) + nested typed structs |
if err := check(err); err != nil |
? operator + miette |
Two-file command split (cmd.go + main.go) |
Single Rust module, split by size |
mu sync.Mutex guard everywhere |
Prefer Arc<T> immutable data + tokio::sync::watch / ArcSwap |
Appendix C — Open questions¶
- O1: Should
Application::builder()accept a pre-builttracingregistry, or always own the subscriber? Leaning toward: accept one. - O2: Should the scaffolder
rtbtool vendor its templates or fetch from Git? Leaning toward: vendored, offline-friendly. - O3: MCP tool schemas — derive from
schemars::JsonSchemaalways, or allow hand-authored JSON Schema override? Likely both. - O4: Plugin discovery beyond
linkme— do we need a dlopen-style runtime plugin story for downstream tools that want third-party commands? Punt until a real user asks.