Skip to main content

basil_bin/
lib.rs

1// SPDX-FileCopyrightText: 2026 OpenBasil Contributors
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Library surface for the unified `basil` binary.
6//!
7//! This crate is primarily a binary (`basil`), but it also exposes its
8//! command-line definition as a library so tooling, notably the `xtask`
9//! man-page generator, can render the command tree without launching the
10//! process. [`cli`] returns the fully assembled clap command.
11
12#![cfg_attr(test, allow(clippy::indexing_slicing))]
13
14pub mod client_cli;
15
16#[cfg(feature = "keystore-backend")]
17use basil_core::demo;
18use basil_core::{agent_cli, bundle_cli, init};
19use clap::{Args, CommandFactory, Parser, Subcommand};
20
21/// The shipped `basil` binary version.
22///
23/// Captured in this crate so it is `basil-bin`'s `CARGO_PKG_VERSION` (the same
24/// value `--version` prints via clap), which the agent threads into
25/// `status`/`health`keeping the reported version in lockstep with the binary
26/// even if `basil-core` is versioned separately.
27pub const VERSION: &str = env!("CARGO_PKG_VERSION");
28
29/// Top-level `basil` command-line interface.
30#[derive(Debug, Parser)]
31#[command(
32    name = "basil",
33    version,
34    about = "Basil broker and operator tool",
35    long_about = "Basil is a host-local secrets broker: your app never touches the key. The \
36                  kernel attests who's calling, a default-deny policy decides, the key is used \
37                  where it lives (OpenBao/Vault, KMS, or a sealed local store), and every \
38                  operation is audited.\n\nThe one `basil` binary is the broker daemon (`basil \
39                  agent`), the offline operator tooling (`init`, `bundle`, `explain`, `doctor`, \
40                  `demo`), and the over-socket client for every broker operation."
41)]
42pub struct Cli {
43    /// Path to the agent's Unix socket for over-socket commands.
44    #[arg(long, env = "BASIL_SOCKET", global = true)]
45    pub socket: Option<String>,
46
47    #[command(subcommand)]
48    pub command: Command,
49}
50
51/// Top-level `basil` subcommands.
52#[derive(Debug, Subcommand)]
53pub enum Command {
54    /// Scaffold a first-run starter set (config, catalog, policy).
55    Init(Box<init::InitArgs>),
56    /// Run a zero-dependency guided tour: scaffold a throwaway broker on the
57    /// built-in keystore backend, start it, and drive a scripted
58    /// sign → verify → denied read → explain → encrypt → mint sequence with
59    /// the audit trail, ending with copy-paste commands to try yourself.
60    #[cfg(feature = "keystore-backend")]
61    Demo(demo::DemoArgs),
62    /// Print a shell completion script for `basil` to stdout. Install it,
63    /// e.g. `basil completions bash > /etc/bash_completion.d/basil` or
64    /// `basil completions fish > ~/.config/fish/completions/basil.fish`.
65    Completions(CompletionsArgs),
66    /// Run the broker daemon.
67    Agent(agent_cli::RunArgs),
68    /// Create and manage a sealed credential bundle.
69    #[command(subcommand)]
70    Bundle(Box<bundle_cli::BundleCommand>),
71    /// Explain a policy decision: why a subject would be allowed or denied an op
72    /// on a key. By DEFAULT this is an offline dry-run: it builds the PDP from
73    /// the catalog + policy FILES on disk and evaluates the tuple through the same
74    /// matcher enforcement uses (no socket, no backend, no secrets). With `--live`
75    /// it instead queries the RUNNING broker's serving generation over the global
76    /// `--socket` (needs the `explain` admin permission). `--effective` previews
77    /// every grant for the subject and is offline-only.
78    Explain(agent_cli::ExplainArgs),
79    /// Preflight environment and deployment checks.
80    Doctor(agent_cli::DoctorArgs),
81    #[command(flatten)]
82    Client(client_cli::Command),
83}
84
85/// `completions` subcommand arguments.
86#[derive(Debug, Args)]
87pub struct CompletionsArgs {
88    /// The shell to emit a completion script for.
89    #[arg(value_enum)]
90    pub shell: clap_complete::Shell,
91}
92
93/// Returns the fully assembled top-level clap [`Command`](clap::Command) for the
94/// `basil` binary, for tooling such as man-page and shell-completion generation.
95#[must_use]
96pub fn cli() -> clap::Command {
97    Cli::command()
98}
99
100/// Render the completion script for `shell` and write it to `out`.
101///
102/// Generation goes through an in-memory buffer so a closed pipe (`basil
103/// completions bash | head`) surfaces as an `Err` instead of a panic inside
104/// the generator.
105pub fn write_completions(
106    shell: clap_complete::Shell,
107    out: &mut dyn std::io::Write,
108) -> std::io::Result<()> {
109    let mut buf = Vec::new();
110    clap_complete::generate(shell, &mut cli(), "basil", &mut buf);
111    out.write_all(&buf)
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    fn parse(args: &[&str]) -> Cli {
119        Cli::try_parse_from(args).expect("parse cli")
120    }
121
122    #[test]
123    fn bundle_is_top_level_command() {
124        let cli = parse(&[
125            "basil",
126            "bundle",
127            "verify",
128            "creds.sealed",
129            "--open",
130            "passphrase:file=/run/pass",
131        ]);
132        assert!(matches!(cli.command, Command::Bundle(_)));
133    }
134
135    #[test]
136    fn old_config_bundle_path_is_not_user_facing() {
137        let err = Cli::try_parse_from(["basil", "config", "bundle", "verify"])
138            .expect_err("config bundle must not remain as a compatibility command");
139        assert!(
140            err.to_string().contains("unrecognized subcommand")
141                || err.to_string().contains("invalid subcommand"),
142            "{err}"
143        );
144    }
145
146    #[test]
147    fn set_backend_accepts_structured_backend_value() {
148        let cli = parse(&[
149            "basil",
150            "bundle",
151            "set-backend",
152            "creds.sealed",
153            "--backend",
154            "id=aws1,type=aws-kms,region=us-east-1,profile=prod",
155            "--open",
156            "passphrase:file=/run/pass",
157        ]);
158        assert!(matches!(cli.command, Command::Bundle(_)));
159    }
160}