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
16use basil_core::{agent_cli, bundle_cli, init};
17use clap::{CommandFactory, Parser, Subcommand};
18
19/// The shipped `basil` binary version.
20///
21/// Captured in this crate so it is `basil-bin`'s `CARGO_PKG_VERSION` (the same
22/// value `--version` prints via clap), which the agent threads into
23/// `status`/`health`keeping the reported version in lockstep with the binary
24/// even if `basil-core` is versioned separately.
25pub const VERSION: &str = env!("CARGO_PKG_VERSION");
26
27/// Top-level `basil` command-line interface.
28#[derive(Debug, Parser)]
29#[command(name = "basil", version, about = "Basil broker and operator tool")]
30pub struct Cli {
31    /// Path to the agent's Unix socket for over-socket commands.
32    #[arg(long, env = "BASIL_SOCKET", global = true)]
33    pub socket: Option<String>,
34
35    #[command(subcommand)]
36    pub command: Command,
37}
38
39/// Top-level `basil` subcommands.
40#[derive(Debug, Subcommand)]
41pub enum Command {
42    /// Scaffold a first-run starter set (config, catalog, policy).
43    Init(Box<init::InitArgs>),
44    /// Run the broker daemon.
45    Agent(agent_cli::RunArgs),
46    /// Create and manage a sealed credential bundle.
47    #[command(subcommand)]
48    Bundle(Box<bundle_cli::BundleCommand>),
49    /// Explain a policy decision: why a subject would be allowed or denied an op
50    /// on a key. By DEFAULT this is an offline dry-run: it builds the PDP from
51    /// the catalog + policy FILES on disk and evaluates the tuple through the same
52    /// matcher enforcement uses (no socket, no backend, no secrets). With `--live`
53    /// it instead queries the RUNNING broker's serving generation over the global
54    /// `--socket` (needs the `explain` admin permission). `--effective` previews
55    /// every grant for the subject and is offline-only.
56    Explain(agent_cli::ExplainArgs),
57    /// Preflight environment and deployment checks.
58    Doctor(agent_cli::DoctorArgs),
59    #[command(flatten)]
60    Client(client_cli::Command),
61}
62
63/// Returns the fully assembled top-level clap [`Command`](clap::Command) for the
64/// `basil` binary, for tooling such as man-page and shell-completion generation.
65#[must_use]
66pub fn cli() -> clap::Command {
67    Cli::command()
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    fn parse(args: &[&str]) -> Cli {
75        Cli::try_parse_from(args).expect("parse cli")
76    }
77
78    #[test]
79    fn bundle_is_top_level_command() {
80        let cli = parse(&[
81            "basil",
82            "bundle",
83            "verify",
84            "creds.sealed",
85            "--open",
86            "passphrase:file=/run/pass",
87        ]);
88        assert!(matches!(cli.command, Command::Bundle(_)));
89    }
90
91    #[test]
92    fn old_config_bundle_path_is_not_user_facing() {
93        let err = Cli::try_parse_from(["basil", "config", "bundle", "verify"])
94            .expect_err("config bundle must not remain as a compatibility command");
95        assert!(
96            err.to_string().contains("unrecognized subcommand")
97                || err.to_string().contains("invalid subcommand"),
98            "{err}"
99        );
100    }
101
102    #[test]
103    fn set_backend_accepts_structured_backend_value() {
104        let cli = parse(&[
105            "basil",
106            "bundle",
107            "set-backend",
108            "creds.sealed",
109            "--backend",
110            "id=aws1,type=aws-kms,region=us-east-1,profile=prod",
111            "--open",
112            "passphrase:file=/run/pass",
113        ]);
114        assert!(matches!(cli.command, Command::Bundle(_)));
115    }
116}