Skip to content

Latest commit

 

History

History
126 lines (90 loc) · 12.1 KB

CONTRIBUTING.md

File metadata and controls

126 lines (90 loc) · 12.1 KB

Contributing to CLI Framework

Library usage: README.md. Doc guides: docs/.

Prerequisites

  • Rust stable (edition 2021; MSRV typically 1.70+)
  • rustfmt + clippy (rustup component add rustfmt clippy)

Clone and build

git clone https://github.com/aroff/cli-framework.git
cd cli-framework
cargo build

Git hooks

A pre-commit hook runs cargo fmt --check and cargo clippy --all-features -- -D warnings before every commit, matching CI exactly.

cp scripts/hooks/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

Repository layout

Path Role
src/ Library
skill/ Bundled Agent Skill (SKILL.md, skill-project.toml, references/)
skill/examples/ Runnable samples — cargo run --example <name>
tests/ [[test]] targets in Cargo.toml
docs/ adr/ (architecture decision records), telemetry.md, testing.md
specs/ In-flight design notes
tools-cli-framework/ Superseded — see skill/

System design (src/)

Module(s) Role
app AppBuilder, App::run, dispatch
command, command::chat Command, registry, categorized root-help metadata and ordering, chat
command_surface, command_surface::tool_bridge Command→tool schemas + shared tool invocation bridge (chat / MCP)
parser, spec argv → args; CommandPath, CommandSpec
plugin registry TOML / manifests
security output sanitize, command risk policy, handle-validated private-file creation/open/atomic replacement
http_retry, retry HTTP retry, secure_reqwest_client
cli_output, cli_mode, message, environment help, environment-variable registry, tables, JSON, modes
api (feature api-server) Built-in Axum host for versioned APIs (/api/{version}/...) plus /healthz + /readyz; build() may apply a root fallback_service (via root_fallback()) as its final composition step. ApiServer::serve(addr) binds and serves; serve_with_listener(listener) is the same path for a listener the caller already bound (an ephemeral :0 bind whose address must be published before serving)
mcp (feature mcp-server) rmcp ServerHandler (tools + resources). Generic, concept-free: per-command with_meta (opaque serde_json::Value) → tools/list top-level _meta; with_visibility (cli-framework acts on it) → _meta.visibility; mcp::resources::ResourceRegistry serves resources via resources/list / resources/read with opaque per-resource with_meta at contents[]._meta. All UI/MCP-Apps semantics live in the consumer (ADR 0066). Optional per-request identity seam: McpRequestAuthenticator turns HTTP request headers into an opaque Arc<dyn Any + Send + Sync>, and McpDynamicToolProvider (McpToolRegistry::with_dynamic_tools, AppBuilder::with_mcp_dynamic_tools) maps that identity to extra Vec<McpDynamicTool> consumed by both tools/list and tools/call so the advertised and callable sets cannot drift — a discovery surface only, static registrations always win, nothing cached across requests (ADR 0081). An McpDynamicTool is { name, command, presentation: Option<McpToolPresentation> }; (String, Command) converts in with .into(). A presentation supplies an owned description + inputSchema for tools whose shape is runtime data rather than a &'static str CommandSpec; it is read by tools/list only (replacing, never merging with, build_input_schema) and never by dispatch, validation or risk classification. mcp serve binds --host/--port itself unless the app handed it a bound listener with AppBuilder::with_mcp_http_listener (then transport_http::start_streamable_http_with_listener serves it, never releasing the port)
telemetry (feature telemetry) OpenTelemetry via the tracing→OTLP bridge (ADR 0068, spec 017/019/020). TelemetryConfig, init_simple/init_batch/init_batch_without_subscriber, TelemetryGuard, Telemetry handle + always-compiled NoopTelemetry. Auto cli.command spans (CLI dispatch, version, and dispatch_tool_call/MCP seams) + cli.command.invocations/cli.command.duration_ms metrics. cli_framework::telemetry::propagation extracts inbound W3C context in ApiServerBuilder and exposes TracedRequestBuilder for outbound injection. Spec 025 adds the telemetry level/attribution/probe axes (axes, probe, probes), deterministic policy resolution with kill switches and the end-user clamp (policy), the framework-owned settings store (<config_dir>/<app>/telemetry.<json|toml>, ADR 0077 — store), the generated telemetry manifest section and <APP>_TELEMETRY_* env layer (manifest, env), the redacting export boundary (ADR 0079 — redact, exporter, resource, init::init_from_policy, init::sampler_for_policy), subscriber composition through a write-once attach slot with a foreign-subscriber warning (ADR 0078 — subscriber::install_default_logging, LoggingGuard), the panic hook (panic), the first-run notice (notice::notice_decision, startup), six doctor checks (doctor::telemetry_checks: telemetry.subscriber, telemetry.store, telemetry.endpoint, telemetry.policy, telemetry.identity, telemetry.env), the bounded end-user flush budget (guard::flush_within) and the end-user Built-in telemetry command group (status/info/set/enable/disable/reset — commands), registered only for an EndUser deployment. App authors configure it through AppBuilder: with_deployment, with_telemetry_defaults (author::TelemetryDefaults), with_telemetry_ops, with_telemetry_identity, with_telemetry_attrs, with_telemetry_never; with_telemetry(TelemetryConfig) and TelemetryConfig::from_env are deprecated in v0.6.0 and removed in v0.8.0. End-user and operator documentation lives in docs/telemetry.md.
self_install (feature self-install) ADR 0080. InstallEnv snapshots the process (OS, home, XDG/known-folder roots, PATH, env vars, TTY) so every operation is testable without touching the real machine. options (SelfInstallOptions), layout (bin dir, receipt path, asset names), receipt (InstallReceipt, versioned JSON), method (script / unmanaged / package-manager inference), place (same-directory temp + rename, Windows .exe.old swap, startup_cleanup), shell_path (shared env/env.fish, rc lines in existing files only), windows_path (HKCU\Environment Path + WM_SETTINGCHANGE), ops (install/uninstall/status, --system, --from), release (GitHub and HTTP sources, channels, downloads with the https/loopback rule), archive (SHA256SUMS, minisign over the sums, safe tar.gz/zip extraction, --version probe), update (self update/self rollback, bin-dir lock and work dir, .prev), policy (self_update.* enforced keys), notice (post-command update notice, daily cache; App::run_with_args runs it after the command so the telemetry level is known), apps_features (Windows HKCU Uninstall entry), commands (the self group, registered by AppBuilder::with_self_install for EndUser only) and doctor (install.* checks). Installer script templates live in skill/references/install-templates/; scripts/ci/self-install-e2e.{sh,ps1} test them against a loopback release, then update to a second demo build (CFW_DEMO_VERSION) and roll back.

Also: auth (feature auth) — TokenProvider trait, AccessToken, AuthError, AuthenticatedHttpClient, four auto-registered auth commands; companion crate cli-framework-oidc provides OidcClient (feature client) and oidc_validation_layer / oidc_validation_layer_multi (feature server, one or several trusted issuers). Its client can route discovery and token requests through validated deployment-owned endpoints while preserving the public issuer as token identity; data_source; observability, testkit behind features; config (feature config) — ConfigBackend/ConfigStore/ConfigHandle (spec 016); FileBackend always, RegistryBackend on Windows only; config::manifest/Policy/resolution (feature config, spec 021) — the plain-JSON config manifest (+ #[derive(ConfigManifest)]), the org Policy document, and the enforced-veto resolver, all networking-free; config::managed (feature config-managed, implies config+auth) — PolicyClient/RoamingConfigClient over AuthenticatedHttpClient; four auto-registered config commands (show/manifest/profile/refresh, feature config-managed) — spec 021's "Command surface", wired via AppBuilder::with_config_manifest/with_policy_client and AppContext::opt_config_manifest/opt_policy_client; config::service (feature config-service, implies config+api-server, spec 022) — the server side: config_service_router (self-authenticating via a crate-local CallerIdentity trait, never ApiServerBuilder::auth()), PolicyStore/UserConfigStore traits with FsPolicyStore/InMemoryUserConfigStore (tests/dev) and PgPolicyStore/PgUserConfigStore (sqlx-core+sqlx-postgres directly, never the sqlx facade) implementations, plus a hand-rolled SQL migration runner.

Flow: AppBuilder registers commands, groups, help ordering, and built-ins into one registry → run resolves id + CommandArgs → await execute on AppContext. Help, completion, command-spec export, chat, and MCP read the same frozen registry. Tool surfaces (chat / MCP) adapt inputs into command_surface::tool_bridge for shared parsing/validation/gating/dispatch.

Externals (summary): Cargo.toml — e.g. tokio, reqwest, clap, serde, aikit-agent; optional comfy-table, indicatif.

Security (summary): sanitize untrusted terminal output; command risk tier policy; plugin paths rooted (no traversal).

The OIDC client's loopback callback lives in cli-framework-oidc/src/client/callback.rs: bounded asynchronous reception owns its sockets for cancellation, validates the request path/state/code, and constructs unambiguous PKCE authorization queries. Browser launch is optional and never blocks callback reception or runtime shutdown.

Features: default ["clap-dispatch", "chat"]; see [features] in Cargo.toml. The clap-dispatch flag is now a no-op default — all dispatch goes through the Clap path. User-visible behavior → update README in the same PR.

Tests

cargo test

Tests run in parallel within a single binary and share the process environment. Any test that mutates env vars (std::env::set_var / remove_var) MUST use a unique, test-scoped name (e.g. CFW_TEST_<TEST>_<PURPOSE>) — generic names like TEST_VAR cause flaky races between tests that touch the same key.

Local CI parity

./scripts/run-ci-tests.sh

Requires cargo install cargo-audit. Matches fmt, clippy -D warnings, audit, release build, tests.

cargo audit ignores

CI runs cargo audit. If an upstream dependency has no compatible patched release available for a RustSec advisory, this repo may temporarily ignore the advisory via .cargo/audit.toml and document why. Any ignores must be treated as temporary and removed once upstream is updated.

Conventions

Documentation status tags

Use these inline tags in README.md, docs/, and specs/ when a documented feature does not match the shipped behavior. Tags MUST appear as the first token of a blockquote so they stand out:

Tag Meaning
[PLANNED] On the roadmap, not yet implemented. Surrounding prose describes the intended shape; the code does not yet do this.
[PARTIAL] Implemented in part. Quote which part works and which does not.
[DEPRECATED] Still works but slated for removal. Link to the replacement.

Every [PLANNED] or [PARTIAL] tag SHOULD link to an ADR or spec that records why the gap exists.

Pull requests

  • Describe change; link issues.
  • API or behavior change → README and/or docs/ in the same PR when it affects integrators.

References