Skip to main content

systemprompt_cli/commands/admin/config/
governance.rs

1//! `admin config governance` — set the authorization hook mode.
2//!
3//! Enforces the mode's invariants (webhook needs a URL; unrestricted needs the
4//! exact acknowledgement sentence) at edit time so a misconfigured governance
5//! block cannot reach the fail-closed bootstrap check.
6
7use anyhow::{Result, bail};
8use clap::{Args, Subcommand};
9use systemprompt_config::ProfileBootstrap;
10use systemprompt_models::profile::{
11    AuthzConfig, AuthzHookConfig, AuthzMode, GovernanceConfig, UNRESTRICTED_ACKNOWLEDGEMENT,
12};
13
14use super::profile_io::{load_profile, save_profile};
15use super::types::ConfigMutationOutput;
16use crate::CliConfig;
17use crate::shared::{CommandOutput, render_result};
18
19#[derive(Debug, Subcommand)]
20pub enum GovernanceCommands {
21    #[command(about = "Show governance configuration")]
22    Show,
23
24    #[command(about = "Set the authorization hook")]
25    Set(SetArgs),
26}
27
28#[derive(Debug, Clone, Args)]
29pub struct SetArgs {
30    #[arg(
31        long,
32        help = "Authz mode: webhook | extension | disabled | unrestricted"
33    )]
34    pub mode: String,
35
36    #[arg(long, help = "Webhook URL (required for mode=webhook)")]
37    pub url: Option<String>,
38
39    #[arg(long, help = "Webhook timeout in milliseconds")]
40    pub timeout_ms: Option<u64>,
41
42    #[arg(
43        long,
44        help = "Acknowledgement sentence (required for mode=unrestricted)"
45    )]
46    pub acknowledgement: Option<String>,
47}
48
49pub fn execute(command: &GovernanceCommands, _config: &CliConfig) -> Result<()> {
50    match command {
51        GovernanceCommands::Show => execute_show(),
52        GovernanceCommands::Set(args) => execute_set(args),
53    }
54}
55
56fn parse_mode(raw: &str) -> Result<AuthzMode> {
57    match raw.to_lowercase().as_str() {
58        "webhook" => Ok(AuthzMode::Webhook),
59        "extension" => Ok(AuthzMode::Extension),
60        "disabled" => Ok(AuthzMode::Disabled),
61        "unrestricted" => Ok(AuthzMode::Unrestricted),
62        other => bail!("unknown authz mode '{other}' (webhook|extension|disabled|unrestricted)"),
63    }
64}
65
66fn execute_set(args: &SetArgs) -> Result<()> {
67    let mode = parse_mode(&args.mode)?;
68
69    if matches!(mode, AuthzMode::Webhook) && args.url.is_none() {
70        bail!("mode=webhook requires --url");
71    }
72    if matches!(mode, AuthzMode::Unrestricted)
73        && args.acknowledgement.as_deref() != Some(UNRESTRICTED_ACKNOWLEDGEMENT)
74    {
75        bail!(
76            "mode=unrestricted requires --acknowledgement \"{}\"",
77            UNRESTRICTED_ACKNOWLEDGEMENT
78        );
79    }
80
81    let profile_path = ProfileBootstrap::get_path()?;
82    let mut profile = load_profile(profile_path)?;
83
84    profile.governance = Some(GovernanceConfig {
85        authz: Some(AuthzConfig {
86            hook: AuthzHookConfig {
87                mode,
88                url: args.url.clone(),
89                timeout_ms: args.timeout_ms.unwrap_or(500),
90                acknowledgement: args.acknowledgement.clone(),
91            },
92        }),
93    });
94
95    save_profile(&profile, profile_path)?;
96
97    render_result(&CommandOutput::card_value(
98        "Governance Updated",
99        &ConfigMutationOutput {
100            field: "governance.authz".to_owned(),
101            message: format!("Authz mode set to {}", args.mode.to_lowercase()),
102        },
103    ));
104    Ok(())
105}
106
107fn execute_show() -> Result<()> {
108    let profile = ProfileBootstrap::get()?;
109    let summary = profile
110        .governance
111        .as_ref()
112        .and_then(|g| g.authz.as_ref())
113        .map_or_else(
114            || "authz: none (fail-closed deny-all)".to_owned(),
115            |authz| {
116                authz.hook.url.as_deref().map_or_else(
117                    || "authz mode set".to_owned(),
118                    |url| format!("authz mode set, url={url}"),
119                )
120            },
121        );
122    render_result(&CommandOutput::text_titled(
123        "Governance Configuration",
124        summary,
125    ));
126    Ok(())
127}