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